'use strict';

/* global angular:false */
/* global showdown:false */
/* global moment:false */
/* global $:false */
/* global ERROR,ISTATES,HSTATES,RSTATES */

// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
if (search.accessToken) {
    localStorage.token = search.accessToken;

    // strip the accessToken and expiresAt, then preserve the rest
    delete search.accessToken;
    delete search.expiresAt;

    // this will reload the page as this is not a hash change
    window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&'));
}

// poor man's async in the global namespace
function asyncForEachParallel(items, handler, callback) {
    var alreadyDone = 0;
    var errored = false;

    if (items.length === 0) return callback();

    function done(error) {
        // do nothing if already called back due to error
        if (errored) return;

        if (error) {
            errored = true;
            return callback(error);
        }

        ++alreadyDone;

        // we are done
        if (alreadyDone === items.length) callback();
    }

    for (var i = 0; i < items.length; ++i) {
        handler(items[i], done);
    }
}

function asyncForEach(items, handler, callback) {
    var cur = 0;

    if (items.length === 0) return callback();

    (function iterator() {
        handler(items[cur], function (error) {
            if (error) return callback(error);
            if (cur >= items.length-1) return callback();
            ++cur;

            iterator();
        });
    })();
}

function asyncSeries(funcs, callback) {
    var cur = 0;

    if (funcs.length === 0) return callback();

    (function iterator() {
        funcs[cur](function (error) {
            if (error) return callback(error);
            if (cur >= funcs.length-1) return callback();
            ++cur;

            iterator();
        });
    })();
}

// create main application module
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);

app.config(['NotificationProvider', function (NotificationProvider) {
    NotificationProvider.setOptions({
        delay: 5000,
        startTop: 60,
        positionX: 'left',
        templateUrl: 'notification.html'
    });
}]);

// configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist
app.config(function ($sceDelegateProvider) {
    $sceDelegateProvider.resourceUrlWhitelist([
        // Allow same origin resource loads.
        'self',
        // Allow loading from our assets domain.
        'https://cloudron.io/**',
        'https://staging.cloudron.io/**',
        'https://dev.cloudron.io/**',
        // Allow local development against the appstore pages
        'http://localhost:5000/**'
    ]);
});

// setup all major application routes
app.config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/', {
        redirectTo: '/apps'
    }).when('/users', {
        controller: 'UsersController',
        templateUrl: 'views/users.html?<%= revision %>'
    }).when('/app/:appId/:view?', {
        controller: 'AppController',
        templateUrl: 'views/app.html?<%= revision %>'
    }).when('/appstore', {
        controller: 'AppStoreController',
        templateUrl: 'views/appstore.html?<%= revision %>'
    }).when('/appstore/:appId', {
        controller: 'AppStoreController',
        templateUrl: 'views/appstore.html?<%= revision %>'
    }).when('/apps', {
        controller: 'AppsController',
        templateUrl: 'views/apps.html?<%= revision %>'
    }).when('/profile', {
        controller: 'ProfileController',
        templateUrl: 'views/profile.html?<%= revision %>'
    }).when('/backups', {
        controller: 'BackupsController',
        templateUrl: 'views/backups.html?<%= revision %>'
    }).when('/graphs', {
        controller: 'GraphsController',
        templateUrl: 'views/graphs.html?<%= revision %>'
    }).when('/network', {
        controller: 'NetworkController',
        templateUrl: 'views/network.html?<%= revision %>'
    }).when('/domains', {
        controller: 'DomainsController',
        templateUrl: 'views/domains.html?<%= revision %>'
    }).when('/email', {
        controller: 'EmailController',
        templateUrl: 'views/email.html?<%= revision %>'
    }).when('/email/:domain', {
        controller: 'EmailController',
        templateUrl: 'views/email.html?<%= revision %>'
    }).when('/notifications', {
        controller: 'NotificationsController',
        templateUrl: 'views/notifications.html?<%= revision %>'
    }).when('/settings', {
        controller: 'SettingsController',
        templateUrl: 'views/settings.html?<%= revision %>'
    }).when('/activity', {
        controller: 'ActivityController',
        templateUrl: 'views/activity.html?<%= revision %>'
    }).when('/support', {
        controller: 'SupportController',
        templateUrl: 'views/support.html?<%= revision %>'
    }).when('/system', {
        controller: 'SystemController',
        templateUrl: 'views/system.html?<%= revision %>'
    }).when('/tokens', {
        controller: 'TokensController',
        templateUrl: 'views/tokens.html?<%= revision %>'
    }).otherwise({ redirectTo: '/'});
}]);

app.filter('installError', function () {
    return function (app) {
        if (!app) return false;
        if (app.installationState === ISTATES.ERROR) return true;
        if (app.installationState === ISTATES.INSTALLED) {
            // app.health can also be null to indicate insufficient data
            if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) return true;
        }

        return false;
    };
});

app.filter('activeTask', function () {
    return function (app) {
        if (!app) return false;
        return app.taskId !== null;
    };
});

app.filter('installSuccess', function () {
    return function (app) {
        if (!app) return false;
        return app.installationState === ISTATES.INSTALLED;
    };
});

app.filter('appIsInstalledAndHealthy', function () {
    return function (app) {
        if (!app) return false;
        return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
    };
});

app.filter('applicationLink', function() {
    return function(app) {
        if (!app) return '';

        if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING && !app.pendingPostInstallConfirmation) {
            return 'https://' + app.fqdn;
        } else {
            return '';
        }
    };
});

app.filter('activeOAuthClients', function () {
    return function (clients, user) {
        return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); });
    };
});

// this appears when an item in app grid is clicked
app.filter('prettyAppErrorMessage', function () {
    return function (app) {
        if (!app) return '';

        if (app.installationState === ISTATES.INSTALLED) {
            // app.health can also be null to indicate insufficient data
            if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.';
        }

        if (app.error.reason === 'Access Denied') {
            if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
        } else if (app.error.reason === 'Already Exists') {
            if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
        }

        return app.error.message;
    };
});

// this appears as tool tip in app grid
app.filter('appProgressMessage', function () {
    return function (app) {
        var message = app.message || (app.error ? app.error.message : '');
        return message;
    };
});

app.filter('selectedTagFilter', function () {
    return function selectedTagFilter(apps, selectedTags) {
        return apps.filter(function (app) {
            if (selectedTags.length === 0) return true;
            if (!app.tags) return false;

            for (var i = 0; i < selectedTags.length; i++) {
                if (app.tags.indexOf(selectedTags[i]) === -1) return false;
            }

            return true;
        });
    };
});

app.filter('selectedDomainFilter', function () {
    return function selectedDomainFilter(apps, selectedDomain) {
        return apps.filter(function (app) {
            if (!selectedDomain) return true;
            if (selectedDomain._alldomains) return true;  // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN

            if (selectedDomain.domain === app.domain) return true;
            return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; });
        });
    };
});

app.filter('prettyDomains', function () {
    return function prettyDomains(domains) {
        return domains.map(function (d) { return d.domain; }).join(', ');
    };
});

app.filter('prettyMemory', function () {
    return function (memory) {
        // Adjust the default memory limit if it changes
        return memory ? Math.floor(memory / 1024 / 1024) : 256;
    };
});

app.filter('prettyDiskSize', function () {
    return function (size) {
        if (!size) return 'not available yet';
        var i = Math.floor(Math.log(size) / Math.log(1024));
        return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; };
});

app.filter('installationActive', function () {
    return function (app) {
        if (app.installationState === ISTATES.ERROR) return false;
        if (app.installationState === ISTATES.INSTALLED) return false;
        return true;
    };
});

// this appears in the app grid
app.filter('installationStateLabel', function () {
    return function(app, user) {
        if (!app) return '';

        var waiting = app.progress === 0 ? ' (Queued)' : '';

        switch (app.installationState) {
        case ISTATES.PENDING_INSTALL:
            return 'Installing' + waiting;
        case ISTATES.PENDING_CLONE:
            return 'Cloning' + waiting;
        case ISTATES.PENDING_LOCATION_CHANGE:
        case ISTATES.PENDING_CONFIGURE:
        case ISTATES.PENDING_RECREATE_CONTAINER:
        case ISTATES.PENDING_DEBUG:
            return 'Configuring' + waiting;
        case ISTATES.PENDING_RESIZE:
            return 'Resizing' + waiting;
        case ISTATES.PENDING_DATA_DIR_MIGRATION:
            return 'Migrating data' + waiting;
        case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
        case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
        case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
        case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
        case ISTATES.PENDING_START: return 'Starting' + waiting;
        case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
        case ISTATES.ERROR: {
            if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
            return 'Error';
        }
        case ISTATES.INSTALLED: {
            if (app.debugMode) {
                return 'Paused';
            } else if (app.runState === 'running') {
                if (!app.health) return 'Starting...'; // no data yet
                if (app.health === HSTATES.HEALTHY) return 'Running';
                return 'Not responding'; // dead/exit/unhealthy
            } else if (app.runState === 'stopped') return 'Stopped';
            else return app.runState;
        }
        default: return app.installationState;
        }
    };
});

app.filter('taskName', function () {
    return function(installationState) {
        switch (installationState) {
        case ISTATES.PENDING_INSTALL: return 'install';
        case ISTATES.PENDING_CLONE: return 'clone';
        case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
        case ISTATES.PENDING_CONFIGURE: return 'configure';
        case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
        case ISTATES.PENDING_DEBUG: return 'debug';
        case ISTATES.PENDING_RESIZE: return 'resize';
        case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
        case ISTATES.PENDING_UNINSTALL: return 'uninstall';
        case ISTATES.PENDING_RESTORE: return 'restore';
        case ISTATES.PENDING_UPDATE: return 'update';
        case ISTATES.PENDING_BACKUP: return 'backup';
        case ISTATES.PENDING_START: return 'start app';
        case ISTATES.PENDING_STOP: return 'stop app';
        default: return installationState || '';
        }
    };
});

app.filter('errorSuggestion', function () {
    return function (error) {
        if (!error) return '';

        switch (error.reason) {
        case ERROR.ACCESS_DENIED:
            if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
            return '';
        case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
        case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
        case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
        case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
        case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
        case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
        case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
        default: return '';
        }
    };
});

app.filter('readyToUpdate', function () {
    return function (apps) {
        return apps.every(function (app) {
            return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED);
        });
    };
});

app.filter('inProgressApps', function () {
    return function (apps) {
        return apps.filter(function (app) {
            return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED;
        });
    };
});

app.filter('prettyHref', function () {
    return function (input) {
        if (!input) return input;
        if (input.indexOf('http://') === 0) return input.slice('http://'.length);
        if (input.indexOf('https://') === 0) return input.slice('https://'.length);
        return input;
    };
});

app.filter('prettyDate', function () {
    // http://ejohn.org/files/pretty.js
    return function prettyDate(time) {
        var date = new Date(time),
            diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
            day_diff = Math.floor(diff / 86400);

        if (isNaN(day_diff) || day_diff < 0)
            return 'just now';

        return day_diff === 0 && (
            diff < 60 && 'just now' ||
                diff < 120 && '1 minute ago' ||
                diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
                diff < 7200 && '1 hour ago' ||
                diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
            day_diff === 1 && 'Yesterday' ||
            day_diff < 7 && day_diff + ' days ago' ||
            day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
            day_diff < 365 && Math.round( day_diff / 30 ) +  ' months ago' ||
                              Math.round( day_diff / 365 ) + ' years ago';
    };
});

app.filter('prettyLongDate', function () {
    return function prettyLongDate(time) {
        return moment(time).format('MMMM Do YYYY, h:mm:ss a');
    };
});

app.filter('prettyShortDate', function () {
    return function prettyShortDate(time) {
        return moment(time).format('MMMM Do YYYY');
    };
});

app.filter('markdown2html', function () {
    var converter = new showdown.Converter({
        extensions: [],
        simplifiedAutoLink: true,
        strikethrough: true,
        tables: true
    });

    return function (text) {
        return converter.makeHtml(text);
    };
});

app.filter('postInstallMessage', function () {
    var SSO_MARKER = '=== sso ===';

    return function (text, app) {
        if (!text) return '';
        if (!app) return text;

        var parts = text.split(SSO_MARKER);
        if (parts.length === 1) {
            // [^] matches even newlines. '?' makes it non-greedy
            if (app.sso) return text.replace(/\<nosso\>[^]*?\<\/nosso\>/g, '');
            else return text.replace(/\<sso\>[^]*?\<\/sso\>/g, '');
        }

        if (app.sso) return parts[1];
        else return parts[0];
    };
});

// custom directive for dynamic names in forms
// See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401
app.directive('laterName', function () {                   // (2)
    return {
        restrict: 'A',
        require: ['?ngModel', '^?form'],                   // (3)
        link: function postLink(scope, elem, attrs, ctrls) {
            attrs.$set('name', attrs.laterName);

            var modelCtrl = ctrls[0];                      // (3)
            var formCtrl  = ctrls[1];                      // (3)
            if (modelCtrl && formCtrl) {
                modelCtrl.$name = attrs.name;              // (4)
                formCtrl.$addControl(modelCtrl);           // (2)
                scope.$on('$destroy', function () {
                    formCtrl.$removeControl(modelCtrl);    // (5)
                });
            }
        }
    };
});

app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) {
    var original = $location.path;
    $location.path = function (path, reload) {
        if (reload === false) {
            var lastRoute = $route.current;
            var un = $rootScope.$on('$locationChangeSuccess', function () {
                $route.current = lastRoute;
                un();
            });
        }
        return original.apply($location, [path]);
    };
}]);

app.directive('ngClickSelect', function () {
    return {
        restrict: 'AC',
        link: function (scope, element/*, attrs */) {
            element.bind('click', function () {
                var selection = window.getSelection();
                var range = document.createRange();
                range.selectNodeContents(this);
                selection.removeAllRanges();
                selection.addRange(range);
            });
        }
    };
});

app.directive('ngClickReveal', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            element.addClass('hand');

            var value = '';

            scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) {
                if (newValue !== oldValue) {
                    element.html('<i>hidden</i>');
                    value = newValue;
                }
            });

            element.bind('click', function () {
                element.text(value);
            });
        }
    };
});

// https://codepen.io/webmatze/pen/isuHh
app.directive('tagInput', function () {
    return {
        restrict: 'E',
        scope: {
            inputTags: '=taglist'
        },
        link: function ($scope, element, attrs) {
            $scope.defaultWidth = 200;
            $scope.tagText = ''; // current tag being edited
            $scope.placeholder = attrs.placeholder;
            $scope.tagArray = function () {
                if ($scope.inputTags === undefined) {
                    return [];
                }
                return $scope.inputTags.split(',').filter(function (tag) {
                    return tag !== '';
                });
            };
            $scope.addTag = function () {
                var tagArray;
                if ($scope.tagText.length === 0) {
                    return;
                }
                tagArray = $scope.tagArray();
                tagArray.push($scope.tagText);
                $scope.inputTags = tagArray.join(',');
                return $scope.tagText = '';
            };
            $scope.deleteTag = function (key) {
                var tagArray;
                tagArray = $scope.tagArray();
                if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) {
                    tagArray.pop();
                } else {
                    if (key !== undefined) {
                        tagArray.splice(key, 1);
                    }
                }
                return $scope.inputTags = tagArray.join(',');
            };
            $scope.$watch('tagText', function (newVal, oldVal) {
                var tempEl;
                if (!(newVal === oldVal && newVal === undefined)) {
                    tempEl = $('<span>' + newVal + '</span>').appendTo('body');
                    $scope.inputWidth = tempEl.width() + 5;
                    if ($scope.inputWidth < $scope.defaultWidth) {
                        $scope.inputWidth = $scope.defaultWidth;
                    }
                    return tempEl.remove();
                }
            });
            element.bind('keydown', function (e) {
                var key = e.which;
                if (key === 9 || key === 13) {
                    e.preventDefault();
                }
                if (key === 8) {
                    return $scope.$apply('deleteTag()');
                }
            });
            element.bind('keyup', function (e) {
                var key = e.which;
                if (key === 9 || key === 13 || key === 32 || key === 188) {
                    e.preventDefault();
                    return $scope.$apply('addTag()');
                }
            });
        },
        template:
            '<div class="tag-input-container">' +
                '<div class="input-tag" data-ng-repeat="tag in tagArray()">' +
                    '{{tag}}' +
                    '<div class="delete-tag" data-ng-click="deleteTag($index)">&times;</div>' +
                '</div>' +
                '<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
            '</div>'
    };
});

app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
    fitTextConfigProvider.config = {
        loadDelay: 250,
        compressor: 0.9,
        min: 8,
        max: 24
    };
}]);
