<template>
  <v-container>
    <v-row>
        <v-treeview
          dense
          activatable
          hoverable
          :multiple-active="false"
          :active.sync="active"
          :open.sync="open"
          :items="treeview"
          style="cursor:pointer;width:100%;"
        >
          <template v-slot:prepend="{item}">
            <template v-if="item.icon">
              <v-icon
                small
                left
                :title="item.leaf ? item.type : `${item.type} (${item.children.length} children)`"
              >{{item.icon}}</v-icon>
            </template>
          </template>

          <template v-slot:label="{item}">
            <template v-if="!('path' in item)">
              {{item.name}}
            </template>
            <template v-else-if="item.leaf">
              <v-chip
                small
                label
                outlined
                :color="item.isArrayItem ? 'secondary' : 'primary'"
              >
                {{item.name}}
              </v-chip>
              <code class="ml-4" v-if="item.type == 'bigint'">{{item.value+"n"}}</code>
              <code class="ml-4" v-else-if="item.type == 'boolean'"><b>{{item.value}}</b></code>
              <code class="ml-4" v-else>{{item.value}}</code>
              <v-icon v-if="item.type == 'string' && (/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3}([0-9a-fA-F]{2})?)?$/.test(item.value) || item.name == 'colour' || item.name == 'color')"
                right
                :color="item.value"
              >mdi-square</v-icon>
            </template>
            <template v-else>
              <v-chip
                small
                label
                outlined
                :color="item.isArrayItem ? 'secondary' : 'primary'"
              >
                {{item.name}}
              </v-chip>
            </template>
          </template>

          <template v-slot:append="{item}">
            <template>
              <v-icon v-if="item.type == 'array'"
                small
                right
                outline
                color="primary"
                title="Add item"
                @click="itemAddDialog(item)"
              >mdi-plus</v-icon>
              <v-icon v-if="item.type == 'object'"
                small
                right
                outline
                color="primary"
                title="Add property"
                @click="itemAddDialog(item)"
              >mdi-plus</v-icon>
              <v-icon v-if="item.type == 'boolean'"
                small
                right
                outline
                color="primary"
                title="Toggle value"
                @click="itemToggle(item)"
              >{{ item.value ? "mdi-checkbox-blank-outline" : "mdi-checkbox-marked-outline" }}</v-icon>
              <v-icon v-if="item.type == 'string' || item.type == 'number'"
                small
                right
                outline
                color="primary"
                title="Edit value"
                @click="itemAddDialog(item, true)"
              >mdi-pencil-outline</v-icon>
              <v-icon
                small
                right
                outlined
                color="red"
                title="Delete"
                :disabled="item.id == rootId"
                @click="itemDelete(item)"
              >mdi-minus</v-icon>
            </template>
          </template>
        </v-treeview>
        <dougal-json-builder-property-dialog
          :open="editor"
          v-model="edit"
          v-bind="editorProperties"
          @save="editorSave"
          @close="editorClose"
        ></dougal-json-builder-property-dialog>
    </v-row>
  </v-container>

</template>

<script>
import { deepValue, deepSet } from '@/lib/utils';
import DougalJsonBuilderPropertyDialog from './property-dialog';

export default {
  name: "DougalJsonBuilder",

  components: {
    DougalJsonBuilderPropertyDialog
  },

  props: {
    value: Object,
    name: String,
    sort: String
  },

  data () {
    const rootId = Symbol("rootNode");
    return {
      rootId,
      active: [],
      open: [ rootId ],
      editor: false,
      editorProperties: {
        nameShown: true,
        nameEditable: true,
        typeShown: true,
        typeEditable: true,
        valueShown: true,
        serialisable: true
      },
      onEditorSave: (evt) => {},
      edit: {
        name: null,
        type: null,
        value: null
      }
    };
  },

  computed: {

    treeview () {

      function sorter (key) {
        return function λ (a, b) {
          return a?.[key] > b?.[key]
            ? 1
            : a?.[key] < b?.[key]
              ? -1
              : 0;
        }
      }

      function getType (value) {
        const t = typeof value;
        switch (t) {
          case "symbol":
          case "string":
          case "bigint":
          case "number":
          case "boolean":
          case "undefined":
            return t;
          case "object":
            return value === null
              ? "null"
              : Array.isArray(value)
                ? "array"
                : t;
        }
      }

      function getIcon (type) {
        switch (type) {
          case "symbol":
            return "mdi-symbol";
          case "string":
            return "mdi-format-text";
          case "bigint":
            return "mdi-numeric";
          case "number":
            return "mdi-numeric";
          case "boolean":
            return "mdi-checkbox-intermediate-variant";
          case "undefined":
            return "mdi-border-none-variant";
          case "null":
            return "mdi-null";
          case "array":
            return "mdi-list-box-outline";
          case "object":
            return "mdi-format-list-bulleted-type";
        }
        return "mdi-help";
      }

      const leaf = ([key, value], parent) => {
        const id = parent
          ? parent.id+"."+key
          : key;
        const name = key;
        const type = getType(value);
        const icon = getIcon(type);
        const isArrayItem = parent?.type == "array";

        const obj = {
          id,
          name,
          type,
          icon,
          isArrayItem,
        };

        if (parent) {
          obj.path = [...parent.path, key];
        } else {
          obj.path = [ key ];
        }

        if (type == "object" || type == "array") {
          const children = [];
          for (const child of Object.entries(value)) {
            children.push(leaf(child, obj));
          }
          if (this.sort) {
            children.sort(sorter(this.sort));
          }
          obj.children = children;
        } else {
          obj.leaf = true;
          obj.value = value;
          /*
          obj.children = [{
            id: id+".value",
            name: String(value)
          }]
          */
        }

        return obj;
      }

      const rootNode = {
        id: this.rootId,
        name: this.name,
        type: getType(this.value),
        icon: getIcon(getType(this.value)),
        children: []
      };
      const view = [rootNode];

      if (this.value) {
        for (const child of Object.entries(this.value)) {
          rootNode.children.push(leaf(child));
        }
        if (this.sort) {
          rootNode.children.sort(sorter(this.sort));
        }
      }

      return view;
    }

  },

  watch: {
    treeview () {
      if (!this.open.includes(this.rootId)) {
        this.open.push(this.rootId);
      }
    }
  },

  methods: {

    openAll (open = true) {
      const walk = (obj) => {
        if (obj?.children) {
          for (const child of obj.children) {
            walk(child);
          }
          if (obj?.id) {
            this.open.push(obj.id);
          }
        }
      }
      for (const item of this.treeview) {
        walk (item);
      }
    },

    itemDelete (item) {
      const parents = [...item.path];
      const key = parents.pop();

      if (key) {

        const value = structuredClone(this.value);
        const obj = parents.length ? deepValue(value, parents) : value;

        if (Array.isArray(obj)) {
          obj.splice(key, 1);
        } else {
          delete obj[key];
        }

        this.$emit("input", value);

      } else {

        this.$emit("input", {});

      }
    },

    itemToggle (item, state) {
      const parents = [...item.path];
      const value = structuredClone(this.value);

      if (parents.length) {
        deepSet(value, parents, state ?? !item.value)
      } else {
        value = state ?? !item.value;
      }

      this.$emit("input", value);
    },

    itemSet (path, content) {
      const parents = [...(path??[])];
      const key = parents.pop();

      if (key !== undefined) {

        const value = structuredClone(this.value);
        const obj = parents.length ? deepValue(value, parents) : value;

        if (Array.isArray(obj)) {
          if (key === null) {
            obj.push(content);
          } else {
            obj[key] = content;
          }
        } else {
          obj[key] = content;
        }

        this.$emit("input", value);

      } else {
        this.$emit("input", content);

      }
    },

    itemAdd (path, content) {
      let value = structuredClone(this.value);
      let path_ = [...(path??[])];

      if (path_.length) {
        try {
          deepSet(value, path_, content);
        } catch (err) {
          if (err instanceof TypeError) {
            this.itemSet(path, content);
            return;
          }
        }
      } else {
        value = content;
      }

      this.$emit("input", value);
    },

    itemAddDialog (item, edit=false) {

      if (!this.open.includes(item.id)) {
        this.open.push(item.id);
      }

      if (edit) {
        this.editorReset({
          name: item.name,
          type: item.type,
          value: item.value
        }, {nameEditable: false});
      } else {
        this.editorReset({}, {
          nameShown: item.type != "array",
          nameRequired: item.type != "array"
        });
      }

      this.onEditorSave = (evt) => {
        this.editor = false;

        let transformer;
        switch(this.edit.type) {
          case "symbol":
            transformer = Symbol;
            break;
          case "string":
            transformer = String;
            break;
          case "bigint":
            transformer = BigInt;
            break;
          case "number":
            transformer = Number;
            break;
          case "boolean":
            transformer = Boolean;
            break;
          case "undefined":
            transformer = () => { return undefined; };
            break;
          case "object":
            transformer = (v) =>
              typeof v == "object"
                ? v
                : (typeof v == "string" && v.length)
                  ? JSON.parse(v)
                  : {};
            break;
          case "null":
            transformer = () => null;
            break;
          case "array":
            // FIXME not great
            transformer = (v) =>
              Array.isArray(v)
                ? v
                : [];
            break;
        }

        const value = transformer(this.edit.value);

        const path = [...(item.path??[])];

        if (!edit) {
          if (item.type == "array") {
            path.push(null);
          } else {
            path.push(this.edit.name);
          }
        }
        this.itemAdd(path, value);
      };
      this.editor = true;

    },

    XXitemEditDialog (item) {

      this.editorReset({
        name: item.name,
        type: item.type,
        value: item.value}, {nameEditable: false});

      this.onEditorSave = (evt) => {
        this.editor = false;

        let transformer;
        switch(this.edit.type) {
          case "symbol":
            transformer = Symbol;
            break;
          case "string":
            transformer = String;
            break;
          case "bigint":
            transformer = BigInt;
            break;
          case "number":
            transformer = Number;
            break;
          case "boolean":
            transformer = Boolean;
            break;
          case "undefined":
            transformer = () => { return undefined; };
            break;
          case "object":
            transformer = (v) =>
              typeof v == "object"
                ? v
                : (typeof v == "string" && v.length)
                  ? JSON.parse(v)
                  : {};
            break;
          case "null":
            transformer = () => null;
            break;
          case "array":
            // FIXME not great
            transformer = (v) =>
              Array.isArray(v)
                ? v
                : [];
            break;
        }

        const key = this.edit.name;
        const value = transformer(this.edit.value);
        this.itemAdd(item, key, value);
      }
      this.editor = true;

    },

    editorReset (values, props) {
      this.edit = {
        name: values?.name,
        type: values?.type,
        value: values?.value
      };

      this.editorProperties = {
        nameShown: props?.nameShown ?? true,
        nameEditable: props?.nameEditable ?? true,
        nameRequired: props?.nameRequired ?? true,
        typeShown: props?.typeShown ?? true,
        typeEditable: props?.typeEditable ?? true,
        valueShown: props?.valueShown ?? true,
        serialisable: props?.serialisable ?? true
      };
    },

    editorSave (evt) {
      this.onEditorSave?.(evt);
    },

    editorClose () {
      this.editor = false;
    }
  }
}

</script>
