"use strict";

require.config({ paths: { 'vs': '3rdparty/vs' }});

// create main application module
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ngDrag', 'ui.bootstrap', 'ui.bootstrap.contextMenu']);

angular.module('Application').filter('prettyOwner', function () {
    return function (uid) {
        if (uid === 0) return 'root';
        if (uid === 33) return 'www-data';
        if (uid === 1000) return 'cloudron';
        if (uid === 1001) return 'git';

        return uid;
    };
});

// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
app.config(function ($sceProvider) {
    $sceProvider.enabled(false);
});

app.filter('trustUrl', ['$sce', function ($sce) {
    return function (recordingUrl) {
        return $sce.trustAsResourceUrl(recordingUrl);
    };
}]);

// https://stackoverflow.com/questions/25621321/angularjs-ng-drag
var ngDragEventDirectives = {};
angular.forEach(
    'drag dragend dragenter dragexit dragleave dragover dragstart drop'.split(' '),
    function(eventName) {
        var directiveName = 'ng' + eventName.charAt(0).toUpperCase() + eventName.slice(1);

        ngDragEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse/*, $rootScope */) {
            return {
                restrict: 'A',
                compile: function($element, attr) {
                    var fn = $parse(attr[directiveName], null, true);

                    return function ngDragEventHandler(scope, element) {
                        element.on(eventName, function(event) {
                            var callback = function() {
                                fn(scope, {$event: event});
                            };

                            scope.$apply(callback);
                        });
                    };
                }
            };
        }];
    }
);
angular.module('ngDrag', []).directive(ngDragEventDirectives);

app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Client', function ($scope, $translate, $timeout, Client) {
    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; }, {});

    $scope.initialized = false;
    $scope.status = null;
    $scope.busy = true;
    $scope.client = Client;
    $scope.cwd = null;
    $scope.cwdParts = [];
    $scope.id = search.appId || search.volumeId;
    $scope.type = search.appId ? 'app' : 'volume';
    $scope.rootDirLabel = '';
    $scope.title = '';
    $scope.entries = [];
    $scope.selected = [];   // holds selected entries
    $scope.clipboard = [];  // holds cut or copied entries
    $scope.clipboardCut = false; // if action is cut or copy
    $scope.dropToBody = false;
    $scope.sortAsc = true;
    $scope.sortProperty = 'fileName';
    $scope.view = 'fileTree';
    $scope.volumes = [];

    $scope.owners = [
        { name: 'cloudron', value: 1000 },
        { name: 'www-data', value: 33 },
        { name: 'git', value: 1001 },
        { name: 'root', value: 0 }
    ];

    function isArchive(f) {
        return f.match(/\.tgz$/)        ||
               f.match(/\.tar$/)        ||
               f.match(/\.7z$/)         ||
               f.match(/\.zip$/)        ||
               f.match(/\.tar\.gz$/)    ||
               f.match(/\.tar\.xz$/)    ||
               f.match(/\.tar\.bz2$/);
    }

    $scope.menuOptions = [];        // shown for entries
    $scope.menuOptionsBlank = [];   // shown for empty space in folder

    var LANGUAGES = [];
    require(['vs/editor/editor.main'], function() {
        LANGUAGES = monaco.languages.getLanguages();
    });

    function getLanguage(filename) {
        var ext = '.' + filename.split('.').pop();
        var language = LANGUAGES.find(function (l) { return !!l.extensions.find(function (e) { return e === ext; }); }) || '';
        return language ? language.id : '';
    }

    function sanitize(filePath) {
        filePath = filePath.split('/').filter(function (a) { return !!a; }).reduce(function (a, v) {
            if (v === '.'); // do nothing
            else if (v === '..') a.pop();
            else a.push(v);
            return a;
        }, []).map(function (p) {
            // small detour to safely handle special characters and whitespace
            return encodeURIComponent(decodeURIComponent(p));
        }).join('/');

        return filePath;
    }

    function openPath(path) {
        path = sanitize(path);

        var parentPath = sanitize(path + '/..');

        // nothing changes here, mostly triggered when editor is closed
        if ($scope.cwd === path) {
            $scope.busy = false;
            return;
        }

        if ($scope.cwd === parentPath) {
            var entry = $scope.entries.find(function (e) { return path === sanitize(parentPath + '/' + e.fileName); });
            if (!entry) return Client.error('No such file or folder: ' + path);

            if (entry.isDirectory) {
                $scope.changeDirectory(path);
            } else if (entry.isFile) {
                var mimeType = Mimer().get(entry.fileName);
                var mimeGroup = mimeType.split('/')[0];

                if (mimeGroup === 'video' || mimeGroup === 'image') {
                    $scope.mediaViewer.show(entry);
                } else if (mimeType === 'application/pdf') {
                    Client.filesGet($scope.id, $scope.type, path, 'open', function (error) { if (error) return Client.error(error); });
                } else if (mimeGroup === 'text' || mimeGroup === 'application') {
                    $scope.textEditor.show(entry);
                } else {
                    Client.filesGet($scope.id, $scope.type, path, 'open', function (error) { if (error) return Client.error(error); });
                }
            }

            $scope.busy = false;

            return;
        }

        Client.filesGet($scope.id, $scope.type, parentPath, 'data', function (error, result) {
            if (error) return Client.error(error);

            // amend icons
            result.entries.forEach(function (e) {
                e.icon = 'fa-file';

                if (e.isDirectory) e.icon = 'fa-folder';
                if (e.isSymbolicLink) e.icon = 'fa-link';
                if (e.isFile) {
                    var mimeType = Mimer().get(e.fileName);
                    var mimeGroup = mimeType.split('/')[0];
                    if (mimeGroup === 'text') e.icon = 'fa-file-alt';
                    if (mimeGroup === 'image') e.icon = 'fa-file-image';
                    if (mimeGroup === 'video') e.icon = 'fa-file-video';
                    if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
                    if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
                    if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
                }
            });

            $scope.entries = result.entries;
            $scope.cwd = parentPath;
            $scope.cwdParts = parentPath.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });

            // call itself now that we know
            openPath(path);
        });
    }

    $scope.isSelected = function (entry) {
        return $scope.selected.indexOf(entry) !== -1;
    };

    $scope.extractStatus = {
        error: null,
        busy: false,
        fileName: ''
    };

    function extract(entry) {
        var filePath = sanitize($scope.cwd + '/' + entry.fileName);

        if (entry.isDirectory) return;

        // prevent it from getting closed
        $('#extractModal').modal({
            backdrop: 'static',
            keyboard: false
        });

        $scope.extractStatus.fileName = entry.fileName;
        $scope.extractStatus.error = null;
        $scope.extractStatus.busy = true;

        Client.filesExtract($scope.id, $scope.type, filePath, function (error) {
            $scope.extractStatus.busy = false;

            if (error) {
                console.error(error);
                $scope.extractStatus.error = $translate.instant('filemanager.extract.error', error);
                return;
            }

            $('#extractModal').modal('hide');

            $scope.refresh();
        });
    }

    function download(entry) {
        var filePath = sanitize($scope.cwd + '/' + entry.fileName);

        Client.filesGet($scope.id, $scope.type, filePath, 'download', function (error) {
            if (error) return Client.error(error);
        });
    }

    $scope.dragStart = function ($event, entry) {
        $event.originalEvent.dataTransfer.setData('text/plain', entry.fileName);
        $event.originalEvent.dataTransfer.setData('application/cloudron-filemanager', entry.fileName);
    };

    $scope.dragEnter = function ($event, entry) {
        $event.originalEvent.stopPropagation();
        $event.originalEvent.preventDefault();

        // if entry is string, we come from breadcrumb
        if (entry && typeof entry === 'string') $event.currentTarget.classList.add('entry-hovered');
        else if (entry && entry.isDirectory) entry.hovered = true;
        else $scope.dropToBody = true;

        $event.originalEvent.dataTransfer.dropEffect = 'move';
    };

    $scope.dragExit = function ($event, entry) {
        $event.originalEvent.stopPropagation();
        $event.originalEvent.preventDefault();

        // if entry is string, we come from breadcrumb
        if (entry && typeof entry === 'string') $event.currentTarget.classList.remove('entry-hovered');
        else if (entry && entry.isDirectory) entry.hovered = false;
        $scope.dropToBody = false;

        $event.originalEvent.dataTransfer.dropEffect = 'move';
    };

    $scope.drop = function (event, entry) {
        event.originalEvent.stopPropagation();
        event.originalEvent.preventDefault();

        $scope.dropToBody = false;

        if (!event.originalEvent.dataTransfer.items[0]) return;

        var targetFolder;
        if (typeof entry === 'string') targetFolder = sanitize(entry);
        else targetFolder = sanitize($scope.cwd + '/' + (entry && entry.isDirectory ? entry.fileName : ''));

        var dataTransfer = event.originalEvent.dataTransfer;

        // check if we have internal drag'n'drop
        if (dataTransfer.getData('application/cloudron-filemanager')) {
            if ($scope.selected.length === 0) return;

            var moved = 0;

            // move files
            async.eachLimit($scope.selected, 5, function (entry, callback) {
                var oldFilePath = sanitize($scope.cwd + '/' + entry.fileName);
                var newFilePath = sanitize(targetFolder + '/' + entry.fileName);

                // if we drop the item on itself
                if (oldFilePath === targetFolder) return callback();

                // if nothing changes
                if (newFilePath === oldFilePath) return callback();

                moved++;

                // TODO this will overwrite files in destination!
                Client.filesRename($scope.id, $scope.type, oldFilePath, newFilePath, callback);
            }, function (error) {
                if (error) return Client.error(error);

                $scope.selected = [];

                // only refresh if anything has changed
                if (moved) $scope.refresh();
            });

            return;
        }

        // figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
        var folderItem;
        try {
            folderItem = dataTransfer.items[0].webkitGetAsEntry();
            if (folderItem.isFile) return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
        } catch (e) {
            return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
        }

        // if we got here we have a folder drop and a modern browser
        // now traverse the folder tree and create a file list
        $scope.uploadStatus.busy = true;
        $scope.uploadStatus.count = 0;

        var fileList = [];
        function traverseFileTree(item, path, callback) {
            if (item.isFile) {
                // Get file
                item.file(function (file) {
                    fileList.push(file);
                    ++$scope.uploadStatus.count;
                    callback();
                });
            } else if (item.isDirectory) {
                // Get folder contents
                var dirReader = item.createReader();
                dirReader.readEntries(function (entries) {
                    async.each(entries, function (entry, callback) {
                        traverseFileTree(entry, path + item.name + '/', callback);
                    }, callback);
                });
            }
        }

        traverseFileTree(folderItem, '', function (error) {
            $scope.uploadStatus.busy = false;
            $scope.uploadStatus.count = 0;

            if (error) return console.error(error);

            uploadFiles(fileList, targetFolder, false);
        });
    };

    $scope.refresh = function () {
        $scope.busy = true;

        Client.filesGet($scope.id, $scope.type, $scope.cwd, 'data', function (error, result) {
            $scope.busy = false;
            if (error) return Client.error(error);

            // amend icons
            result.entries.forEach(function (e) {
                e.icon = 'fa-file';

                if (e.isDirectory) e.icon = 'fa-folder';
                if (e.isSymbolicLink) e.icon = 'fa-link';
                if (e.isFile) {
                    var mimeType = Mimer().get(e.fileName);
                    var mimeGroup = mimeType.split('/')[0];
                    if (mimeGroup === 'text') e.icon = 'fa-file-alt';
                    if (mimeGroup === 'image') e.icon = 'fa-file-image';
                    if (mimeGroup === 'video') e.icon = 'fa-file-video';
                    if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
                    if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
                    if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
                }
            });

            $scope.entries = result.entries;
        });
    };

    $scope.open = function (entry) {
        location.hash = sanitize($scope.cwd + '/' + entry.fileName);
    };

    $scope.select = function ($event, entry) {
        // we don't stop propagation for context menu closing, but if targets don't match we got the whole list click event
        if ($event.currentTarget !== $event.target) return;

        if (!entry) {
            $scope.selected = [];
            return;
        }

        var i = $scope.selected.indexOf(entry);
        var multi = ($event.ctrlKey || $event.metaKey);

        if ($event.button === 0) {
            // left click

            if (multi) {
                if (i === -1) $scope.selected.push(entry);
                else $scope.selected.splice(i, 1);
            } else {
                $scope.selected = [ entry ];
            }
        } else if ($event.button === 2) {
            // right click

            if (i === -1) {
                if (multi) $scope.selected.push(entry);
                else $scope.selected = [ entry ];
            }
        }
    };

    $scope.onEntryContextMenu = function ($event, entry) {
        if ($scope.selected.indexOf(entry) !== -1) return;
        $scope.selected.push(entry);
    };

    $scope.actionCut = function () {
        $scope.clipboard = $scope.selected.slice();
        $scope.clipboard.forEach(function (entry) {
            entry.fullFilePath = sanitize($scope.cwd + '/' + entry.fileName);
        });
        $scope.clipboardCut = true;
    };

    $scope.actionCopy = function () {
        $scope.clipboard = $scope.selected.slice();
        $scope.clipboard.forEach(function (entry) {
            entry.fullFilePath = sanitize($scope.cwd + '/' + entry.fileName);
        });
        $scope.clipboardCut = false;
    };

    function collectFiles(entry, callback) {
        if (entry.isDirectory) {
            Client.filesGet($scope.id, $scope.type, entry.fullFilePath, 'data', function (error, result) {
                if (error) return callback(error);
                if (!result.entries) return callback(new Error('not a folder'));

                // amend fullFilePath
                result.entries.forEach(function (e) {
                    e.fullFilePath = sanitize(entry.fullFilePath + '/' + e.fileName);
                });

                var collectedFiles = [];
                async.eachLimit(result.entries, 5, function (entry, callback) {
                    collectFiles(entry, function (error, result) {
                        if (error) return callback(error);

                        collectedFiles = collectedFiles.concat(result);

                        callback();
                    });
                }, function (error) {
                    if (error) return callback(error);

                    callback(null, collectedFiles);
                });
            });
            return;
        }

        callback(null, [ entry ]);
    }

    $scope.actionPaste = function (destinationEntry) {
        if ($scope.clipboardCut) {
            // move files
            async.eachLimit($scope.clipboard, 5, function (entry, callback) {
                var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fileName);

                // TODO this will overwrite files in destination!
                Client.filesRename($scope.id, $scope.type, entry.fullFilePath, newFilePath, callback);
            }, function (error) {
                if (error) return Client.error(error);

                // clear clipboard
                $scope.clipboard = [];

                $scope.refresh();
            });
        } else {
            // copy files

            // first collect all files recursively
            var collectedFiles = [];

            async.eachLimit($scope.clipboard, 5, function (entry, callback) {
                collectFiles(entry, function (error, result) {
                    if (error) return callback(error);

                    collectedFiles = collectedFiles.concat(result);

                    callback();
                });
            }, function (error) {
                if (error) return Client.error(error);

                async.eachLimit(collectedFiles, 5, function (entry, callback) {
                    var newFilePath = sanitize($scope.cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath);

                    // This will NOT overwrite but finds a unique new name to copy to
                    Client.filesCopy($scope.id, $scope.type, entry.fullFilePath, newFilePath, callback);
                }, function (error) {
                    if (error) return Client.error(error);

                    // clear clipboard
                    $scope.clipboard = [];

                    $scope.refresh();
                });
            });
        }
    };

    $scope.actionSelectAll = function () {
        $scope.selected = $scope.entries.slice();
    };

    $scope.goDirectoryUp = function () {
        $scope.changeDirectory($scope.cwd + '/..');
    };

    $scope.changeDirectory = function (path) {
        path = sanitize(path);

        if ($scope.cwd === path) return;

        location.hash = path;

        $scope.cwd = path;
        $scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });

        $scope.refresh();
    };

    $scope.uploadStatus = {
        error: null,
        busy: false,
        fileName: '',
        count: 0,
        countDone: 0,
        size: 0,
        done: 0,
        percentDone: 0,
        files: [],
        targetFolder: ''
    };

    function uploadFiles(files, targetFolder, overwrite) {
        if (!files || !files.length) return;

        targetFolder = targetFolder || $scope.cwd;
        overwrite = !!overwrite;

        // prevent it from getting closed
        $('#uploadModal').modal({
            backdrop: 'static',
            keyboard: false
        });

        $scope.uploadStatus.files = files;
        $scope.uploadStatus.targetFolder = targetFolder;
        $scope.uploadStatus.error = null;
        $scope.uploadStatus.busy = true;
        $scope.uploadStatus.count = files.length;
        $scope.uploadStatus.countDone = 0;
        $scope.uploadStatus.size = 0;
        $scope.uploadStatus.sizeDone = 0;
        $scope.uploadStatus.done = 0;
        $scope.uploadStatus.percentDone = 0;

        for (var i = 0; i < files.length; ++i) {
            $scope.uploadStatus.size += files[i].size;
        }

        async.eachSeries(files, function (file, callback) {
            var filePath = sanitize(targetFolder + '/' + (file.webkitRelativePath || file.name));

            $scope.uploadStatus.fileName = file.name;

            Client.filesUpload($scope.id, $scope.type, filePath, file, overwrite, function (loaded) {
                $scope.uploadStatus.percentDone = ($scope.uploadStatus.done+loaded) * 100 / $scope.uploadStatus.size;
                $scope.uploadStatus.sizeDone = loaded;
            }, function (error) {
                if (error) return callback(error);

                $scope.uploadStatus.done += file.size;
                $scope.uploadStatus.percentDone = $scope.uploadStatus.done * 100 / $scope.uploadStatus.size;
                $scope.uploadStatus.countDone++;

                callback();
            });
        }, function (error) {
            $scope.uploadStatus.busy = false;

            if (error && error.statusCode === 409) {
                $scope.uploadStatus.error = 'exists';
                return;
            } else if (error) {
                console.error(error);
                $scope.uploadStatus.error = 'generic';
                return;
            }

            $('#uploadModal').modal('hide');

            $scope.uploadStatus.fileName = '';
            $scope.uploadStatus.count = 0;
            $scope.uploadStatus.size = 0;
            $scope.uploadStatus.sizeDone = 0;
            $scope.uploadStatus.done = 0;
            $scope.uploadStatus.percentDone = 100;
            $scope.uploadStatus.files = [];
            $scope.uploadStatus.targetFolder = '';

            $scope.refresh();
        });
    }

    $scope.retryUpload = function (overwrite) {
        uploadFiles($scope.uploadStatus.files, $scope.uploadStatus.targetFolder, !!overwrite);
    };

    // file upload
    $('#uploadFileInput').on('change', function (e) { uploadFiles(e.target.files || [], $scope.cwd, false); });
    $scope.onUploadFile = function () { $('#uploadFileInput').click(); };

    // folder upload
    $('#uploadFolderInput').on('change', function (e ) { uploadFiles(e.target.files || [], $scope.cwd, false); });
    $scope.onUploadFolder = function () { $('#uploadFolderInput').click(); };

    $scope.restartAppBusy = false;
    $scope.onRestartApp = function () {
        $scope.restartAppBusy = true;

        function waitUntilRestarted(callback) {
            Client.getApp($scope.id, function (error, result) {
                if (error) return callback(error);

                if (result.installationState === ISTATES.INSTALLED) return callback();
                setTimeout(waitUntilRestarted.bind(null, callback), 2000);
            });
        }

        Client.restartApp($scope.id, function (error) {
            if (error) console.error('Failed to restart app.', error);

            waitUntilRestarted(function (error) {
                if (error) console.error('Failed wait for restart.', error);

                $scope.restartAppBusy = false;
            });
        });
    };

    $scope.newDirectory = {
        busy: false,
        error: null,
        name: '',

        show: function () {
            $scope.newDirectory.error = null;
            $scope.newDirectory.name = '';
            $scope.newDirectory.busy = false;

            $scope.newDirectoryForm.$setUntouched();
            $scope.newDirectoryForm.$setPristine();

            $('#newDirectoryModal').modal('show');
        },

        submit: function () {
            $scope.newDirectory.busy = true;
            $scope.newDirectory.error = null;

            var filePath = sanitize($scope.cwd + '/' + $scope.newDirectory.name);

            Client.filesCreateDirectory($scope.id, $scope.type, filePath, function (error) {
                $scope.newDirectory.busy = false;
                if (error && error.statusCode === 409) return $scope.newDirectory.error = 'exists';
                if (error) return Client.error(error);

                $scope.refresh();

                $('#newDirectoryModal').modal('hide');
            });
        }
    };

    $scope.newFile = {
        busy: false,
        error: null,
        name: '',

        show: function () {
            $scope.newFile.error = null;
            $scope.newFile.name = '';
            $scope.newFile.busy = false;

            $scope.newFileForm.$setUntouched();
            $scope.newFileForm.$setPristine();

            $('#newFileModal').modal('show');
        },

        submit: function () {
            $scope.newFile.busy = true;
            $scope.newFile.error = null;

            var filePath = sanitize($scope.cwd + '/' + $scope.newFile.name);

            Client.filesUpload($scope.id, $scope.type, filePath, new File([], $scope.newFile.name), false, function () {}, function (error) {
                $scope.newFile.busy = false;
                if (error && error.statusCode === 409) return $scope.newFile.error = 'exists';
                if (error) return Client.error(error);

                $scope.refresh();

                $('#newFileModal').modal('hide');
            });
        }
    };

    $scope.renameEntry = {
        busy: false,
        error: null,
        entry: null,
        newName: '',

        show: function (entry) {
            $scope.renameEntry.error = null;
            $scope.renameEntry.entry = entry;
            $scope.renameEntry.newName = entry.fileName;
            $scope.renameEntry.busy = false;

            $('#renameEntryModal').modal('show');
        },

        submit: function () {
            $scope.renameEntry.busy = true;

            var oldFilePath = sanitize($scope.cwd + '/' + $scope.renameEntry.entry.fileName);
            var newFilePath = sanitize(($scope.renameEntry.newName[0] === '/' ? '' : ($scope.cwd + '/')) + $scope.renameEntry.newName);

            Client.filesRename($scope.id, $scope.type, oldFilePath, newFilePath, function (error) {
                $scope.renameEntry.busy = false;
                if (error) return Client.error(error);

                $scope.refresh();

                $('#renameEntryModal').modal('hide');
            });
        }
    };

    $scope.mediaViewer = {
        type: '',
        src: '',
        entry: null,

        show: function (entry) {
            var filePath = sanitize($scope.cwd + '/' + entry.fileName);

            $scope.mediaViewer.entry = entry;
            $scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0];
            $scope.mediaViewer.src = '';

            Client.filesGet($scope.id, $scope.type, filePath, 'link', function (error, result) {
                if (error) return Client.error(error);

                $scope.mediaViewer.src = result;

                $('#mediaViewerModal').modal('show');
            });
        },

        close: function () {
            $('#mediaViewerModal').modal('hide');
        }
    };

    $scope.textEditor = {
        busy: false,
        entry: null,
        editor: null,
        unsaved: false,

        show: function (entry) {
            $scope.textEditor.entry = entry;
            $scope.textEditor.busy = false;
            $scope.textEditor.unsaved = false;

            // clear model if any
            if ($scope.textEditor.editor && $scope.textEditor.editor.getModel()) $scope.textEditor.editor.setModel(null);

            $scope.view = 'textEditor';
            // document.getElementById('textEditorModal').style['display'] = 'flex';

            var filePath = sanitize($scope.cwd + '/' + entry.fileName);
            var language = getLanguage(entry.fileName);

            Client.filesGet($scope.id, $scope.type, filePath, 'data', function (error, result) {
                if (error) return Client.error(error);

                if (!$scope.textEditor.editor) {
                    $timeout(function () {
                        $scope.textEditor.editor = monaco.editor.create(document.getElementById('textEditorContainer'), {
                            value: result,
                            language: language,
                            theme: 'vs-dark'
                        });
                        $scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; });
                    }, 200);
                } else {
                    $scope.textEditor.editor.setModel(monaco.editor.createModel(result, language));
                    $scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); // have to re-attach whenever model changes
                }
            });
        },

        save: function (callback) {
            $scope.textEditor.busy = true;

            var newContent = $scope.textEditor.editor.getValue();
            var filePath = sanitize($scope.cwd + '/' + $scope.textEditor.entry.fileName);
            var file = new File([newContent], 'file');

            Client.filesUpload($scope.id, $scope.type, filePath, file, true, function () {}, function (error) {
                if (error) return Client.error(error);

                // update size immediately for the list view to avoid reloading the whole list
                $scope.textEditor.entry.size = file.size;

                $timeout(function () {
                    $scope.textEditor.unsaved = false;
                    $scope.textEditor.busy = false;
                    if (typeof callback === 'function') return callback();
                }, 1000);
            });
        },

        close: function () {
            $scope.view = 'fileTree';
            $('#textEditorCloseModal').modal('hide');
        },

        saveAndClose: function () {
            $scope.textEditor.save(function () {
                $scope.textEditor.close();
            });
        },

        maybeClose: function () {
            if (!$scope.textEditor.unsaved) return $scope.textEditor.close();
            $('#textEditorCloseModal').modal('show');
        },
    };

    $scope.chownEntry = {
        busy: false,
        error: null,
        newOwner: 0,
        recursive: false,
        showRecursiveOption: false,

        show: function () {
            $scope.chownEntry.error = null;
            // set default uid from first file
            $scope.chownEntry.newOwner = $scope.selected[0].uid;
            $scope.chownEntry.busy = false;

            // default for directories is recursive
            $scope.chownEntry.recursive = !!$scope.selected.find(function (entry) { return entry.isDirectory; });
            $scope.chownEntry.showRecursiveOption = !!$scope.chownEntry.recursive;

            $('#chownEntryModal').modal('show');
        },

        submit: function () {
            $scope.chownEntry.busy = true;

            async.eachLimit($scope.selected, 5, function (entry, callback) {
                var filePath = sanitize($scope.cwd + '/' + entry.fileName);

                Client.filesChown($scope.id, $scope.type, filePath, $scope.chownEntry.newOwner, $scope.chownEntry.recursive, callback);
            }, function (error) {
                $scope.chownEntry.busy = false;
                if (error) return Client.error(error);

                $scope.refresh();

                $('#chownEntryModal').modal('hide');
            });
        }
    };

    $scope.entryRemove = {
        busy: false,
        error: null,

        show: function () {
            $scope.entryRemove.error = null;

            $('#entryRemoveModal').modal('show');
        },

        submit: function () {
            $scope.entryRemove.busy = true;

            async.eachLimit($scope.selected, 5, function (entry, callback) {
                var filePath = sanitize($scope.cwd + '/' + entry.fileName);

                Client.filesRemove($scope.id, $scope.type, filePath, callback);
            }, function (error) {
                $scope.entryRemove.busy = false;
                if (error) return Client.error(error);

                $scope.refresh();

                $('#entryRemoveModal').modal('hide');
            });

        }
    };

    function fetchVolumesInfo(mounts) {
        $scope.volumes = [];

        async.each(mounts, function (mount, callback) {
            Client.getVolume(mount.volumeId, function (error, result) {
                if (error) return callback(error);

                $scope.volumes.push(result);

                callback();
            });
        }, function (error) {
            if (error) console.error('Failed to fetch volumes info.', error);
        });
    }

    function init() {

        Client.getStatus(function (error, status) {
            if (error) return Client.initError(error, init);

            if (!status.activated) {
                console.log('Not activated yet, redirecting', status);
                window.location.href = '/';
                return;
            }

            // check version and force reload if needed
            if (!localStorage.version) {
                localStorage.version = status.version;
            } else if (localStorage.version !== status.version) {
                localStorage.version = status.version;
                window.location.reload(true);
            }

            $scope.status = status;

            console.log('Running filemanager version ', localStorage.version);

            // get user profile as the first thing. this populates the "scope" and affects subsequent API calls
            Client.refreshUserInfo(function (error) {
                if (error) return Client.initError(error, init);

                var getter = $scope.type === 'app' ? Client.getApp.bind(Client, $scope.id) : Client.getVolume.bind(Client, $scope.id);

                getter(function (error, result) {
                    if (error) {
                        $scope.initialized = true;
                        return;
                    }

                    // fine to do async
                    fetchVolumesInfo(result.mounts || []);

                    $scope.title = $scope.type === 'app' ? (result.label || result.fqdn) : result.name;
                    $scope.rootDirLabel = $scope.type === 'app' ? '/app/data/' : result.hostPath;
                    window.document.title = $scope.title + ' - ' + $translate.instant('filemanager.title');

                    // now mark the Client to be ready
                    Client.setReady();

                    openPath(window.location.hash.slice(1));

                    $scope.initialized = true;
                });
            });
        });
    }

    init();

    window.addEventListener('hashchange', function () {
        $scope.$apply(function () {
            // first close all dialogs
            $scope.mediaViewer.close();
            $scope.textEditor.close();

            openPath(window.location.hash.slice(1));
        });
    });

    function isModalVisible() {
        return !!document.getElementsByClassName('modal in').length;
    }

    // handle save shortcuts
    window.addEventListener('keydown', function (event) {
        if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
            if ($scope.view !== 'textEditor') return;

            event.preventDefault();
            $scope.$apply($scope.textEditor.save);
        } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'c') {
            if ($scope.view === 'textEditor') return;
            if ($scope.selected.length === 0) return;
            if (isModalVisible()) return;

            event.preventDefault();
            $scope.$apply($scope.actionCopy);
        } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'x') {
            if ($scope.view === 'textEditor') return;
            if ($scope.selected.length === 0) return;
            if (isModalVisible()) return;

            event.preventDefault();
            $scope.$apply($scope.actionCut);
        } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'v') {
            if ($scope.view === 'textEditor') return;
            if ($scope.clipboard.length === 0) return;
            if (isModalVisible()) return;

            event.preventDefault();
            $scope.$apply($scope.actionPaste);
        } else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'a') {
            if ($scope.view === 'textEditor') return;
            if (isModalVisible()) return;

            event.preventDefault();
            $scope.$apply($scope.actionSelectAll);
        } else if(event.key === 'Escape') {
            $scope.$apply(function () { $scope.selected = []; });
        }
    });

    $translate(['filemanager.list.menu.edit', 'filemanager.list.menu.cut', 'filemanager.list.menu.copy', 'filemanager.list.menu.paste', 'filemanager.list.menu.rename', 'filemanager.list.menu.chown', 'filemanager.list.menu.extract', 'filemanager.list.menu.download', 'filemanager.list.menu.delete' ]).then(function (tr) {
        $scope.menuOptions = [
            {
                text: tr['filemanager.list.menu.edit'],
                displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && !entry.isSymbolicLink; },
                enabled: function () { return $scope.selected.length === 1; },
                hasBottomDivider: true,
                click: function ($itemScope, $event, entry) { $scope.open(entry); }
            }, {
                text: tr['filemanager.list.menu.cut'],
                click: function ($itemScope, $event, entry) { $scope.actionCut(); }
            }, {
                text: tr['filemanager.list.menu.copy'],
                click: function ($itemScope, $event, entry) { $scope.actionCopy(); }
            }, {
                text: tr['filemanager.list.menu.paste'],
                hasBottomDivider: true,
                enabled: function () { return $scope.clipboard.length; },
                click: function ($itemScope, $event, entry) { $scope.actionPaste(entry); }
            }, {
                text: tr['filemanager.list.menu.rename'],
                enabled: function () { return $scope.selected.length === 1; },
                click: function ($itemScope, $event, entry) { $scope.renameEntry.show(entry); }
            }, {
                text: tr['filemanager.list.menu.chown'],
                click: function ($itemScope, $event, entry) { $scope.chownEntry.show(); }
            }, {
                text: tr['filemanager.list.menu.extract'],
                displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && isArchive(entry.fileName); },
                click: function ($itemScope, $event, entry) { extract(entry); }
            }, {
                text: tr['filemanager.list.menu.download'],
                enabled: function () { return $scope.selected.length === 1; },
                click: function ($itemScope, $event, entry) { download(entry); }
            }, {
                text: tr['filemanager.list.menu.delete'],
                hasTopDivider: true,
                click: function ($itemScope, $event, entry) { $scope.entryRemove.show(); }
            }
        ];
    });

    $translate(['filemanager.toolbar.newFile', 'filemanager.toolbar.newFolder', 'filemanager.list.menu.paste', 'filemanager.list.menu.selectAll' ]).then(function (tr) {
        $scope.menuOptionsBlank = [
            {
                text: tr['filemanager.toolbar.newFile'],
                click: function ($itemScope, $event) { $scope.newFile.show(); }
            }, {
                text: tr['filemanager.toolbar.newFolder'],
                click: function ($itemScope, $event) { $scope.newDirectory.show(); }
            }, {
                text: tr['filemanager.list.menu.paste'],
                hasTopDivider: true,
                hasBottomDivider: true,
                enabled: function () { return $scope.clipboard.length; },
                click: function ($itemScope, $event) { $scope.actionPaste(); }
            }, {
                text: tr['filemanager.list.menu.selectAll'],
                click: function ($itemScope, $event) { $scope.actionSelectAll(); }
            }
        ];
    });

    $('.file-list').on('scroll', function (event) {
        if (event.target.scrollTop > 10) event.target.classList.add('top-scroll-indicator');
        else event.target.classList.remove('top-scroll-indicator');
    });

    // setup all the dialog focus handling
    ['newFileModal', 'newDirectoryModal', 'renameEntryModal'].forEach(function (id) {
        $('#' + id).on('shown.bs.modal', function () {
            $(this).find('[autofocus]:first').focus();
        });
    });

    // this will update the hash
    $('#mediaViewerModal').on('hidden.bs.modal', function () {
        location.hash = $scope.cwd;
    });

    // selects filename (without extension)
    ['renameEntryModal'].forEach(function (id) {
        $('#' + id).on('shown.bs.modal', function () {
            var elem = $(this).find('[autofocus]:first');
            var text = elem.val();
            elem[0].setSelectionRange(0, text.indexOf('.'));
        });
    });
}]);
