<style>
    .my-editor {
        background: #2d2d2d;
        color: #ccc;
        font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
        font-size: 14px;
        line-height: 1.5;
        padding: 5px;
    }

    .prism-editor__textarea:focus {
        outline: none;
    }

    .config-editor-overlay div.v-card {
        position: relative;
    }

    .config-editor-overlay div.v-card header {
        position: sticky;
        top: 0;
        width: 100%;
        z-index: 1;
    }
</style>

<template>
    <v-row>
        <v-col>
            <v-card>
                <v-card-title>
                    {{ $t('Setting.ConfigFiles') }}
                    <v-spacer class="d-none d-sm-block"></v-spacer>
                    <input type="file" ref="fileUpload" style="display: none" multiple @change="uploadFile" />
                    <v-item-group class="v-btn-toggle my-5 my-sm-0 col-12 col-sm-auto px-0 py-0">
                        <v-btn v-if="currentPath !== '' && currentPath !== '/config_examples'" class="flex-grow-1" @click="uploadFileButton" :loading="loadings.includes['configFileUpload']"><v-icon>mdi-file-upload</v-icon></v-btn>
                        <v-btn v-if="currentPath !== '' && currentPath !== '/config_examples'" class="flex-grow-1" @click="createFile"><v-icon>mdi-file-plus</v-icon></v-btn>
                        <v-btn v-if="currentPath !== '' && currentPath !== '/config_examples'" class="flex-grow-1" @click="createFolder"><v-icon>mdi-folder-plus</v-icon></v-btn>
                        <v-btn class="flex-grow-1" @click="refreshFileList"><v-icon>mdi-refresh</v-icon></v-btn>
                        <v-menu :offset-y="true" title="Setup current list">
                            <template v-slot:activator="{ on, attrs }">
                                <v-btn class="flex-grow-1" v-bind="attrs" v-on="on"><v-icon class="">mdi-cog</v-icon></v-btn>
                            </template>
                            <v-list>
                                <v-list-item class="minHeight36">
                                    <v-checkbox class="mt-0" hide-details v-model="showHiddenFiles" :label="$t('Setting.HiddenFiles')"></v-checkbox>
                                </v-list-item>
                            </v-list>
                        </v-menu>
                    </v-item-group>
                </v-card-title>
                <v-card-subtitle>{{ $t('Setting.CurrentPath') }} {{ this.currentPath === "" ? "/" : this.currentPath }}</v-card-subtitle>
                <v-data-table
                    :items="files"
                    class="files-table"
                    :headers="headers"
                    :options="options"
                    :page.sync="currentPage"
                    :custom-sort="sortFiles"
                    :sort-by.sync="sortBy"
                    :sort-desc.sync="sortDesc"
                    :items-per-page.sync="countPerPage"
                    :footer-props="{
                        itemsPerPageText:  $t('Setting.Files') 
                    }"
                    mobile-breakpoint="0"
                    item-key="name">

                    <template #no-data>
                        <div class="text-center">{{ $t('Setting.Empty')  }}</div>
                    </template>

                    <template slot="body.prepend" v-if="(currentPath !== '')">
                       <tr
                            class="file-list-cursor"
                            @click="clickRowGoBack">
                            <td class="pr-0 text-center" style="width: 32px;"><v-icon>mdi-folder-upload</v-icon></td>
                            <td class=" " colspan="8">..</td>
                        </tr>
                    </template>

                    <template #item="{ item }">
                        <tr
                            @contextmenu="showContextMenu($event, item)"
                            @click="clickRow(item)"
                            class="file-list-cursor"
                            :data-name="item.filename">
                            <td class="pr-0 text-center" style="width: 32px;">
                                <v-icon v-if="item.isDirectory">mdi-folder</v-icon>
                                <v-icon v-if="!item.isDirectory">mdi-file</v-icon>
                            </td>
                            <td class=" ">{{ item.filename }}</td>
                            <td class="text-no-wrap text-right">{{ item.isDirectory ? '--' : formatFilesize(item.size) }}</td>
                            <td class="text-right">{{ formatDate(item.modified) }}</td>
                        </tr>
                    </template>
                </v-data-table>
            </v-card>
            <v-menu v-model="contextMenu.shown" :position-x="contextMenu.x" :position-y="contextMenu.y" absolute offset-y>
                <v-list>
                    <v-list-item @click="clickRow(contextMenu.item)" v-if="!contextMenu.item.isDirectory">
                        <v-icon class="mr-1">mdi-file-document-edit-outline</v-icon>  {{ $t('Setting.EditFile') }}
                    </v-list-item>
                    <v-list-item @click="downloadFile" v-if="!contextMenu.item.isDirectory">
                        <v-icon class="mr-1">mdi-cloud-download</v-icon> {{ $t('Setting.Download') }}
                    </v-list-item>
                    <v-list-item @click="renameFile(contextMenu.item)" v-if="!contextMenu.item.isDirectory && currentPath !== '/config_examples'">
                        <v-icon class="mr-1">mdi-rename-box</v-icon> {{ $t('Setting.Rename') }}
                    </v-list-item>
                    <v-list-item @click="removeFile" v-if="!contextMenu.item.isDirectory && currentPath !== '/config_examples'">
                        <v-icon class="mr-1">mdi-delete</v-icon> {{ $t('Setting.Delete') }}
                    </v-list-item>
                    <v-list-item @click="deleteDirectoryAction" v-if="contextMenu.item.isDirectory && currentPath !== '' && currentPath !== '/config_examples'">
                        <v-icon class="mr-1">mdi-delete</v-icon> {{ $t('Setting.Delete') }}
                    </v-list-item>
                </v-list>
            </v-menu>
            <v-dialog v-model="editor.showLoader" hide-overlay persistent width="300" >
                <v-card color="primary" dark >
                    <v-card-text>
                        {{ $t('Setting.PleaseStandBy') }}
                        <v-progress-linear indeterminate color="white" class="mb-0" ></v-progress-linear>
                    </v-card-text>
                </v-card>
            </v-dialog>
            <v-dialog v-model="editor.show" fullscreen hide-overlay transition="dialog-bottom-transition" content-class="config-editor-overlay">
                <v-card d-flex>
                    <v-toolbar dark color="primary">
                        <v-btn icon dark @click="editor.show = false">
                            <v-icon>mdi-close</v-icon>
                        </v-btn>
                        <v-toolbar-title>{{ editor.item.filename }}</v-toolbar-title>
                        <v-spacer></v-spacer>
                        <v-toolbar-items>
                            <v-btn dark text href="https://www.klipper3d.org/Config_Reference.html" target="_blank" class="d-none d-md-flex"><v-icon small class="mr-1">mdi-help</v-icon>{{ $t('Setting.ConfigReference') }}</v-btn>
                            <v-divider white vertical v-if="currentPath !== '/config_examples'" class="d-none d-md-flex"></v-divider>
                            <v-btn dark text @click="saveFile(false)" v-if="currentPath !== '/config_examples'"><v-icon small class="mr-1">mdi-content-save</v-icon><span class="d-none d-sm-inline">{{ $t('Setting.Save') }}</span></v-btn>
                            <v-divider white vertical v-if="currentPath !== '/config_examples' && !['printing', 'paused'].includes(printer_state)" class="d-none d-sm-flex"></v-divider>
                            <v-btn dark text @click="saveFile(true)" v-if="currentPath !== '/config_examples' && !['printing', 'paused'].includes(printer_state)" class="d-none d-sm-flex"><v-icon small class="mr-1">mdi-restart</v-icon>{{ $t('Setting.SaveRestart') }}</v-btn>
                        </v-toolbar-items>
                    </v-toolbar>
                    <prism-editor class="my-editor" v-model="editor.sourcecode" :readonly="editor.readonly" :highlight="highlighter" line-numbers></prism-editor>
                </v-card>
            </v-dialog>
            <v-dialog v-model="dialogRenameFile.show" max-width="400">
                <v-card>
                    <v-card-title class="headline">{{ $t('Setting.RenameFile') }}</v-card-title>
                    <v-card-text>
                        <v-text-field  :label="$t('Setting.Name')" required v-model="dialogRenameFile.newName"></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn color="" text @click="dialogRenameFile.show = false">{{ $t('Setting.Cancel') }}</v-btn>
                        <v-btn color="primary" text @click="renameFileAction">{{ $t('Setting.Rename') }}</v-btn>
                    </v-card-actions>
                </v-card>
            </v-dialog>
            <v-dialog v-model="dialogCreateFile.show" max-width="400">
                <v-card>
                    <v-card-title class="headline">{{ $t('Setting.CreateFile') }}</v-card-title>
                    <v-card-text>
                        <v-text-field  :label="$t('Setting.Name')" required v-model="dialogCreateFile.name"></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn color="" text @click="dialogCreateFile.show = false">{{ $t('Setting.Cancel') }}</v-btn>
                        <v-btn color="primary" text @click="createFileAction">{{ $t('Setting.Create') }}</v-btn>
                    </v-card-actions>
                </v-card>
            </v-dialog>
            <v-dialog v-model="dialogCreateFolder.show" max-width="400">
                <v-card>
                    <v-card-title class="headline">{{ $t('Setting.CreateFolder') }}</v-card-title>
                    <v-card-text>
                        <v-text-field :label="$t('Setting.Name')" required v-model="dialogCreateFolder.name"></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn color="" text @click="dialogCreateFolder.show = false">{{ $t('Setting.Cancel') }}</v-btn>
                        <v-btn color="primary" text @click="createFolderAction">{{ $t('Setting.Create') }}</v-btn>
                    </v-card-actions>
                </v-card>
            </v-dialog>
            <v-snackbar
                :timeout="-1"
                :value="true"
                fixed
                right
                bottom
                dark
                v-model="uploadSnackbar.status"
            >
                <span v-if="uploadSnackbar.max > 1" class="mr-1">({{ uploadSnackbar.number }}/{{ uploadSnackbar.max }})</span><strong>{{ $t('Setting.Uploading') }} {{ uploadSnackbar.filename }}</strong><br />
                {{ Math.round(uploadSnackbar.percent) }} % @ {{ formatFilesize(Math.round(uploadSnackbar.speed)) }}/s<br />
                <v-progress-linear class="mt-2" :value="uploadSnackbar.percent"></v-progress-linear>
                <template v-slot:action="{ attrs }">
                    <v-btn
                        color="red"
                        text
                        v-bind="attrs"
                        @click="cancelUpload"
                        style="min-width: auto;"
                    >
                        <v-icon class="0">mdi-close</v-icon>
                    </v-btn>
                </template>
            </v-snackbar>
        </v-col>
    </v-row>
</template>

<script>
    import { mapState } from 'vuex'
    import {findDirectory} from "@/plugins/helpers";

    import { PrismEditor } from 'vue-prism-editor';
    import 'vue-prism-editor/dist/prismeditor.min.css'; // import the styles somewhere

    // import highlighting library (you can use any library you want just return html string)
    import { highlight, languages } from 'prismjs/components/prism-core';
    import 'prismjs/components/prism-editorconfig';
    import 'prismjs/components/prism-ini';
    import 'prismjs/themes/prism-okaidia.css';
    import axios from "axios"; // import syntax highlighting styles

    export default {
        components: {
            PrismEditor,
        },
        data: function() {
            return {
                sortBy: 'filename',
                sortDesc: false,
                selected: [],
                headers: [
                    { text: '', value: '', },
                    { text: this.$t('Setting.Name'), value: 'filename', },
                    { text: this.$t('Setting.Filesize'), value: 'size', align: 'right', },
                    { text: this.$t('Setting.LastModified'), value: 'modified', align: 'right', },
                ],
                options: {
                },
                currentPage: 1,
                files: [],
                currentPath: '/config',
                editor: {
                    show: false,
                    showLoader: false,
                    readonly: false,
                    item: {
                        filename: "",
                    },
                    sourcecode: ''
                },
                contextMenu: {
                    shown: false,
                    isDirectory: false,
                    touchTimer: undefined,
                    x: 0,
                    y: 0,
                    item: {}
                },
                dialogRenameFile: {
                    show: false,
                    newName: "",
                    item: {}
                },
                dialogCreateFile: {
                    show: false,
                    name: "",
                },
                dialogCreateFolder: {
                    show: false,
                    name: "",
                },
                uploadSnackbar: {
                    status: false,
                    filename: "",
                    percent: 0,
                    speed: 0,
                    total: 0,
                    number: 0,
                    max: 0,
                    cancelTokenSource: "",
                    lastProgress: {
                        time: 0,
                        loaded: 0
                    }
                }
            }
        },
        computed: {
            ...mapState({
                filetree: state => state.files.filetree,
                hostname: state => state.socket.hostname,
                port: state => state.socket.port,
                loadings: state => state.socket.loadings,
                printer_state: state => state.printer.print_stats.state,
            }),
            countPerPage: {
                get: function() {
                    return this.$store.state.gui.settings.configfiles.countPerPage
                },
                set: function(newVal) {
                    return this.$store.dispatch("gui/setSettings", { settings: { configfiles: { countPerPage: newVal } } });
                }
            },
            showHiddenFiles: {
                get: function() {
                    return this.$store.state.gui.settings.configfiles.showHiddenFiles
                },
                set: function(newVal) {
                    return this.$store.dispatch("gui/setSettings", { settings: { configfiles: { showHiddenFiles: newVal } } })
                }
            },
        },
        created() {
            this.loadPath();
        },
        methods: {
            highlighter(code) {
                return highlight(code, languages.ini); //returns html
            },
            refreshFileList: function() {
                if (this.currentPath === "") {
                    this.$socket.sendObj('server.files.get_directory', { path: 'config' }, 'files/getDirectory');
                    this.$socket.sendObj('server.files.get_directory', { path: 'config_examples' }, 'files/getDirectory');
                } else this.$socket.sendObj('server.files.get_directory', { path: this.currentPath.substring(1) }, 'files/getDirectory');
            },
            formatDate(date) {
                let tmp2 = new Date(date);

                return tmp2.toLocaleString().replace(',', '');
            },
            formatFilesize(fileSizeInBytes) {
                let i = -1;
                let byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'];
                do {
                    fileSizeInBytes = fileSizeInBytes / 1024;
                    i++;
                } while (fileSizeInBytes > 1024);

                return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i];
            },
            sortFiles(items, sortBy, sortDesc) {
                sortBy = sortBy.length ? sortBy[0] : 'filename';
                sortDesc = sortDesc[0];

                if (items !== undefined) {
                    // Sort by index
                    items.sort(function(a, b) {
                        if (a[sortBy] === b[sortBy]) {
                            return 0;
                        }
                        if (a[sortBy] === null || a[sortBy] === undefined) {
                            return -1;
                        }
                        if (b[sortBy] === null || b[sortBy] === undefined) {
                            return 1;
                        }
                        if (a[sortBy].constructor === String && b[sortBy].constructor === String) {
                            return a[sortBy].localeCompare(b[sortBy], undefined, { sensivity: 'base' });
                        }
                        if (a[sortBy] instanceof Array && b[sortBy] instanceof Array) {
                            const reducedA = a[sortBy].length ? a.filament.reduce((a, b) => a + b) : 0;
                            const reducedB = b[sortBy].length ? b.filament.reduce((a, b) => a + b) : 0;
                            return reducedA - reducedB;
                        }
                        return a[sortBy] - b[sortBy];
                    });

                    // Deal with descending order
                    if (sortDesc) items.reverse();

                    // Then make sure directories come first
                    items.sort((a, b) => (a.isDirectory === b.isDirectory) ? 0 : (a.isDirectory ? -1 : 1));
                }
                return items;
            },
            loadPath: function() {
                let dirArray = this.currentPath.substring(1).split("/");

                this.files = findDirectory(this.filetree, dirArray);
                if (dirArray.length === 1 && this.currentPath === "") {
                    this.files = this.files.filter(element => !["gcodes", "docs"].includes(element.filename))
                }

                if (!this.showHiddenFiles) {
                    this.files = this.files.filter(file => file.filename.substr(0, 1) !== ".");
                }
            },
            clickRow: function(item) {
                if (!item.isDirectory) {
                    this.editor.showLoader = true;
                    this.editor.sourcecode = "";
                    this.editor.item = item;

                    let url = '//'+this.hostname+':'+this.port+'/server/files'+this.currentPath+'/'+item.filename+'?time='+Date.now();
                    fetch(url, { cache: "no-cache" }).then(res => res.text()).then(file => {
                        this.editor.sourcecode = file;
                        this.editor.show = true;
                        this.editor.showLoader = false;
                        this.editor.readonly = false;
                        if (this.currentPath === '/config_examples') this.editor.readonly = true;
                    });

                } else {
                    this.currentPath += "/"+item.filename;
                    this.currentPage = 1;
                }
            },
            clickRowGoBack: function() {
                this.currentPath = this.currentPath.substr(0, this.currentPath.lastIndexOf("/"));
            },
            saveFile: function(boolRestart = false) {
                let file = new File([this.editor.sourcecode], this.editor.item.filename);

                let formData = new FormData();
                formData.append('file', file);
                formData.append('root', 'config');
                if(this.currentPath.length > 7) formData.append('path', this.currentPath.substring(8));

                axios.post('//' + this.hostname + ':' + this.port + '/server/files/upload',
                    formData, {
                        headers: { 'Content-Type': 'multipart/form-data' }
                    }
                ).then(() => {
                    this.$toast.success("File '"+this.editor.item.filename+"' successfully saved.");
                    this.editor.show = false;
                    this.editor.sourcecode = "";

                    if (boolRestart && this.editor.item.filename === "moonraker.conf") {
                        this.$socket.sendObj('machine.services.restart', { service: "moonraker" })
                    } else if (boolRestart) {
                        this.$store.commit('server/addEvent', { message: "FIRMWARE_RESTART", type: 'command' })
                        this.$socket.sendObj('printer.gcode.script', { script: "FIRMWARE_RESTART" })
                    }
                }).catch(() => {
                    this.$toast.error("Error save "+this.editor.item.filename);
                });
            },
            showContextMenu (e, item) {
                e.preventDefault();
                this.contextMenu.shown = true;
                this.contextMenu.x = e.clientX;
                this.contextMenu.y = e.clientY;
                this.contextMenu.item = item;
                this.$nextTick(() => {
                    this.contextMenu.shown = true
                });
            },
            downloadFile() {
                let filename = (this.currentPath+"/"+this.contextMenu.item.filename);
                let link = document.createElement("a");
                link.download = name;
                link.href = '//' + this.hostname + ':' + this.port + '/server/files' + filename;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            },
            createFolder() {
                this.dialogCreateFolder.name = "";
                this.dialogCreateFolder.show = true;
            },
            createFolderAction() {
                this.dialogCreateFolder.show = false;

                this.$socket.sendObj('server.files.post_directory', {
                    path: this.currentPath.substring(1)+"/"+this.dialogCreateFolder.name
                }, 'files/getCreateDir');
            },
            createFile() {
                this.dialogCreateFile.name = "";
                this.dialogCreateFile.show = true;
            },
            createFileAction() {
                let file = new File([""], this.dialogCreateFile.name);

                let formData = new FormData();
                formData.append('file', file);
                formData.append('root', 'config');
                if(this.currentPath.length > 7) formData.append('path', this.currentPath.substring(8));

                axios.post('//' + this.hostname + ':' + this.port + '/server/files/upload',
                    formData, {
                        headers: { 'Content-Type': 'multipart/form-data' }
                    }
                ).then(() => {
                    this.dialogCreateFile.show = false;
                    this.dialogCreateFile.name = "";
                }).catch(() => {
                    window.console.error("Error save "+this.editor.item.filename);
                });
            },
            renameFile(item) {
                this.dialogRenameFile.item = item;
                this.dialogRenameFile.newName = item.filename;
                this.dialogRenameFile.show = true;
            },
            renameFileAction() {
                this.dialogRenameFile.show = false;
                this.$socket.sendObj('server.files.move', {
                    source: this.currentPath+"/"+this.dialogRenameFile.item.filename,
                    dest: this.currentPath+"/"+this.dialogRenameFile.newName
                }, 'files/getMove');
            },
            removeFile() {
                this.$socket.sendObj('server.files.delete_file', { path: this.currentPath+"/"+this.contextMenu.item.filename }, 'files/getDeleteFile');
            },
            deleteDirectoryAction: function() {
                this.$socket.sendObj('server.files.delete_directory', { path: this.currentPath+"/"+this.contextMenu.item.filename }, 'files/getDeleteDir');
            },
            uploadFileButton: function() {
                this.$refs.fileUpload.click()
            },
            async uploadFile() {
                if (this.$refs.fileUpload.files.length) {
                    this.$store.commit('socket/addLoading', { name: 'configFileUpload' })
                    let successFiles = []
                    this.uploadSnackbar.number = 0
                    this.uploadSnackbar.max = this.$refs.fileUpload.files.length
                    for (const file of this.$refs.fileUpload.files) {
                        this.uploadSnackbar.number++
                        const result = await this.doUploadFile(file)
                        successFiles.push(result)
                    }

                    this.$store.commit('socket/removeLoading', { name: 'configFileUpload' })
                    for(const file of successFiles) {
                        this.$toast.success("Upload of "+file+" successful!")
                    }

                    this.$refs.fileUpload.value = ''
                }
            },
            doUploadFile: function(file) {
                let toast = this.$toast;
                let formData = new FormData();
                let filename = file.name.replace(" ", "_");

                this.uploadSnackbar.filename = filename
                this.uploadSnackbar.status = true
                this.uploadSnackbar.percent = 0
                this.uploadSnackbar.speed = 0
                this.uploadSnackbar.lastProgress.loaded = 0
                this.uploadSnackbar.lastProgress.time = 0

                formData.append('root', 'config');
                formData.append('file', file, (this.currentPath+"/"+filename).substring(7));
                this.$store.commit('socket/addLoading', { name: 'configFileUpload' });

                return new Promise(resolve => {
                    this.uploadSnackbar.cancelTokenSource = axios.CancelToken.source();
                    axios.post('//' + this.hostname + ':' + this.port + '/server/files/upload',
                        formData, {
                            headers: { 'Content-Type': 'multipart/form-data' },
                            cancelToken: this.uploadSnackbar.cancelTokenSource.token,
                            onUploadProgress: (progressEvent) => {
                                this.uploadSnackbar.percent = (progressEvent.loaded * 100) / progressEvent.total
                                if (this.uploadSnackbar.lastProgress.time) {
                                    const time = progressEvent.timeStamp - this.uploadSnackbar.lastProgress.time
                                    const data = progressEvent.loaded - this.uploadSnackbar.lastProgress.loaded

                                    if (time) this.uploadSnackbar.speed = data / (time / 1000)
                                }

                                this.uploadSnackbar.lastProgress.time = progressEvent.timeStamp
                                this.uploadSnackbar.lastProgress.loaded = progressEvent.loaded
                                this.uploadSnackbar.total = progressEvent.total
                            }
                        }
                    ).then((result) => {
                        this.uploadSnackbar.status = false
                        resolve(result.data.result)
                    }).catch(() => {
                        this.uploadSnackbar.status = false
                        this.$store.commit('socket/removeLoading', { name: 'configFileUpload' });
                        toast.error("Cannot upload the file!");
                    })
                })
            },
            cancelUpload: function() {
                this.uploadSnackbar.cancelTokenSource.cancel()
                this.uploadSnackbar.status = false
            },
        },
        watch: {
            config: {
                deep: true,
                handler() {
                    this.loadPath();
                }
            },
            config_examples: {
                deep: true,
                handler() {
                    this.loadPath();
                }
            },
            filetree: {
                deep: true,
                handler() {
                    this.loadPath();
                }
            },
            currentPath: {
                handler() {
                    this.loadPath();
                }
            },
            showHiddenFiles: {
                handler() {
                    this.loadPath();
                }
            }
        }
    }
</script>
