<template>
    <div :class="$style.root">
        <div :class="$style.item" :disabled="disabled">
            <div :class="$style.expander" v-if="keys"
                :expanded="currentExpanded" @click="toggle()"></div>
            <span :class="$style.property" v-if="property" @click="toggle()">{{ property }}</span>
            <span :class="$style.colon" v-if="property" @click="toggle()">: </span>
            <span :class="$style.value">
                <span :class="$style.text" :type="valueType" :editing="editing" @dblclick="edit()">{{ formattedValue }}</span>
                <textarea ref="textarea" :class="$style.textarea" v-if="editing" v-model="editingValue" :style="textareaStyle"
                    autofocus @click.stop
                    @input="resizeTextarea($event.target)"
                    @blur="change()"
                    @keydown.enter="change()"
                ></textarea>
            </span>
        </div>
        <div :class="$style.list" v-if="currentExpanded && keys && keys.length < 1000">
            <lcap-json-view-node
                v-for="subKey in keys"
                :key="subKey"
                :property="subKey"
                v-model="currentValue[subKey]"
                :readonly="readonly"
                :disabled="disabled"
            ></lcap-json-view-node>
        </div>
        <slot></slot>
    </div>
</template>

<script>
import { MEmitter } from 'cloud-ui.vusion';
import saferEval from 'safer-eval';

const rawTypeRE = /^\[object (\w+)]$/;
const specialTypeRE = /^\[native \w+ (.*)\]$/;

import isPlainObject from 'lodash/isPlainObject';

export default {
    name: 'lcap-json-view-node',
    rootName: 'lcap-json-view',
    mixins: [MEmitter],
    props: {
        property: String,
        value: null,
        expanded: { type: Boolean, default: false },
        readonly: { type: Boolean, default: false },
        disabled: { type: Boolean, default: false },
    },

    data() {
        return {
            depth: 0,
            currentExpanded: this.expanded,
            editing: false,
            currentValue: this.value,
            editingValue: '',
            nodeVMs: [],
            parentVM: undefined,
            rootVM: undefined,
            textareaStyle: {},
        };
    },
    computed: {
        valueType() {
            const type = typeof this.currentValue;

            if (this.currentValue === null || this.currentValue === undefined)
                return 'null';
            else if (type === 'boolean' || type === 'number' || this.currentValue === Infinity)
                return 'literal';
            else if (specialTypeRE.test(this.currentValue))
                return 'native';
            else if (type === 'string' && !rawTypeRE.test(this.currentValue))
                return 'string';

            return 'null';
        },
        formattedValue() {
            const type = typeof this.currentValue;

            if (this.currentValue === null)
                return 'null';
            else if (this.currentValue === undefined)
                return 'undefined';
            else if (type === 'number' && isNaN(this.currentValue))
                return 'NaN';
            else if (this.currentValue === Infinity)
                return 'Infinity';
            else if (Array.isArray(this.currentValue))
                return 'Array[' + this.currentValue.length + ']';
            else if (isPlainObject(this.currentValue))
                return ('Object' + (Object.keys(this.currentValue).length ? '' : ' (empty)'));
            else if (type === 'object' && !isPlainObject(this.currentValue))
                return (this.currentValue.constructor.name + (Object.keys(this.currentValue).length ? '' : ' (empty)'));
            else if (this.currentValueType === 'native')
                return specialTypeRE.exec(this.currentValue)[1];
            else if (type === 'string') {
                const typeMatch = this.currentValue.match(rawTypeRE);
                if (typeMatch)
                    return typeMatch[1];
                else
                    return JSON.stringify(this.currentValue);
            } else if (type === 'function')
                return 'Function';
            else
                return this.currentValue;
        },
        keys() {
            if (typeof this.currentValue === 'object' && this.currentValue !== null)
                return Object.keys(this.currentValue);
            else
                return null;
        },
    },
    watch: {
        expanded(expanded) {
            this.currentExpanded = expanded;
        },
        value(value) {
            this.currentValue = value;
        },
    },

    created() {
        this.$dispatch(this.$options.name, 'add-node-vm', this);
        !this.parentVM
            && this.$dispatch(this.$options.rootName, 'add-node-vm', this);

        this.$on('add-node-vm', (nodeVM) => {
            nodeVM.rootVM = this.rootVM;
            nodeVM.parentVM = this;
            nodeVM.depth = this.depth + 1;
            if (nodeVM.depth <= nodeVM.rootVM.expandDepth)
                nodeVM.currentExpanded = true;
            this.nodeVMs.push(nodeVM);
        });
        this.$on('remove-node-vm', (nodeVM) => {
            nodeVM.rootVM = undefined;
            nodeVM.parentVM = undefined;
            this.nodeVMs.splice(this.nodeVMs.indexOf(nodeVM), 1);
        });
    },
    destroyed() {
        this.$dispatch(this.$options.rootName, 'remove-node-vm', this);
        this.$dispatch(this.$options.name, 'remove-node-vm', this);
    },
    methods: {
        toggle(expanded) {
            const oldExpanded = this.currentExpanded;

            if (expanded === undefined)
                expanded = !this.currentExpanded;

            if (expanded === oldExpanded)
                return;

            let cancel = false;
            this.$emit('before-toggle', {
                expanded,
                nodeVM: this,
                preventDefault: () => (cancel = true),
            });

            if (cancel)
                return;

            this.currentExpanded = expanded;

            this.$emit('update:expanded', expanded);
            this.$emit('toggle', {
                expanded,
                nodeVM: this,
            });
        },
        edit() {
            if (this.readonly || this.disabled)
                return;

            this.editing = true;

            this.editingValue = JSON.stringify(this.currentValue, (key, value) => {
                if (value === undefined)
                    return '__vusion__undefined__';
                else if (typeof value === 'number' && isNaN(value))
                    return '__vusion__NaN__';
                else if (value === Infinity)
                    return '__vusion__Infinity__';
                return value;
            }).replace(/"__vusion__undefined__"/g, 'undefined')
                .replace(/"__vusion__NaN__"/g, 'NaN')
                .replace(/"__vusion__Infinity__"/g, 'Infinity');

            this.$nextTick(() => {
                this.resizeTextarea(this.$refs.textarea);
                this.$refs.textarea.select();
            });
        },
        change() {
            this.editing = false;
            try {
                const newValue = saferEval(this.editingValue);
                this.$emit('input', newValue);

                const $event = {
                    property: this.property,
                    oldValue: this.currentValue,
                    newValue,
                    nodeVM: this,
                };

                this.$emit('change', $event);
                this.rootVM.change($event);
            } catch (e) {}
        },
        resizeTextarea(textareaEl) {
            textareaEl.style.width = '3px';
            textareaEl.style.height = '3px';
            textareaEl.style.width = textareaEl.scrollWidth - 3 + 'px';
            textareaEl.style.height = textareaEl.scrollHeight + 'px';
        },
    },
};
</script>

<style module>
.root {
    font-family: Menlo, Consolas, monospace;
    margin-left: 20px;
    cursor: default;
}

.item {
    position: relative;
    white-space: nowrap;
}

.item[disabled] {
    opacity: 0.6;
}

.property {
    color: #2c75e4;
}

.value {
    display: inline-block;
    position: relative;
}

.text[type="null"] {
    color: #93989f;
}

.text[type="string"] {
    color: #55a808;
}

.text[type="literal"] {
    color: #e0276e;
}

.text[editing] {
    visibility: hidden;
}

.textarea {
    position: absolute;
    top: -1px;
    left: -4px;
    z-index: 1;
    outline: none;
    border: 1px solid #bbb;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
    font-family: Menlo, Consolas, monospace;
    line-height: inherit;
    box-sizing: content-box;
    resize: none;
    padding: 0 3px;
    white-space: nowrap;
    overflow: hidden;
}

.textarea[role="back"] {
    line-height: 1;
}

.expander {
    position: absolute;
    z-index: 1;
    width: 19px;
    height: 19px;
    text-align: center;
    margin-left: -19px;
    color: #6e6e6e;
}

.expander::before {
    display: inline-block;
    font-size: var(--font-size-large);
    line-height: 18px;
    transition: transform var(--transition-duration-base);
    transform: rotate(90deg);
    icon-font: url('vscode-codicons/src/icons/debug-breakpoint-function.svg');
}

.expander[expanded]::before {
    transform: rotate(180deg);
}

[class^="index__"][theme="one-monakai"] .property {
    color: #e06c75;
}

[class^="index__"][theme="one-monakai"] .text[type="null"] {
    color: #7f848e;
}

[class^="index__"][theme="one-monakai"] .text[type="string"] {
    color: #98c379;
}

[class^="index__"][theme="one-monakai"] .text[type="literal"] {
    color: #d19a66;
}
</style>
