//////////////////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////////////////

import _ from 'lodash';
import pgAdmin from 'sources/pgadmin';

import { FileType } from 'react-aspen';
import { TreeNode } from './tree_nodes';

function manageTreeEvents(event, eventName, item) {
  let d = item ? item._metadata.data : [];
  let node_metadata = item ? item._metadata : {};
  let node;
  let obj = pgAdmin.Browser;

  // Events for preferences tree.
  if (node_metadata.parent?.includes('/preferences') && obj.ptree.tree.type == 'preferences') {
    try {
      obj.Events.trigger(
        'preferences:tree:' + eventName, event, item, d
      );
    } catch (e) {
      console.warn(e.stack || e);
      return false;
    }
  } else if(eventName == 'hovered') {
    /* Raise tree events for the nodes */
    try {
      obj.Events.trigger(
        'pgadmin-browser:tree:' + eventName, item, d, node
      );
    } catch (e) {
      console.warn(e.stack || e);
      return false;
    }
  } else if (d && obj.Nodes[d._type]) {
    // Events for browser tree.
    node = obj.Nodes[d._type];

    // If the Browser tree is not initialised yet
    if (obj.tree === null) return;

    if (eventName == 'dragstart') {
      obj.tree.handleDraggable(event, item);
    }
    if (eventName == 'added' || eventName == 'beforeopen' || eventName == 'loaded') {
      obj.tree.addNewNode(item.getMetadata('data').id, item.getMetadata('data'), item, item.parent.path);
    }
    if(eventName == 'copied') {
      obj.tree.copyHandler?.(item.getMetadata('data'), item);
    }
    if (_.isObject(node.callbacks) &&
      eventName in node.callbacks &&
      typeof node.callbacks[eventName] == 'function') {
      node.callbacks[eventName].apply(node, [item, d, obj, [], eventName]);
    }

    /* Raise tree events for the nodes */
    try {
      obj.Events.trigger(
        'pgadmin-browser:tree:' + eventName, item, d, node
      );
    } catch (e) {
      console.warn(e.stack || e);
      return false;
    }
  }
  return true;
}


export class Tree {
  constructor(tree, manageTree, pgBrowser, type) {
    this.tree = tree;
    this.tree.type = type || 'browser';
    this.tree.onTreeEvents(manageTreeEvents);

    this.rootNode = manageTree.tempTree;
    this.Nodes = pgBrowser ? pgBrowser.Nodes : pgAdmin.Browser.Nodes;

    // Flag to suppress added and opened tree event being called during object search operations,
    // tree.select of search object clashes with other tree.select.
    this.suppressEventsForPath = null;
    this.draggableTypes = {};
  }

  async refresh(item) {
    //  Set _children to null as empty array not reload the children nodes on refresh.
    if(item.children?.length == 0) {
      item._children = null;
    }
    await this.tree.refresh(item);
  }

  async add(item, data) {
    await this.tree.create(item.parent, data.itemData);
  }

  async before(item, data) {
    return Promise.resolve(await this.tree.create(item.parent, data));
  }

  async update(item, data) {
    await this.tree.update(item, data);
  }

  async remove(item) {
    await this.tree.remove(item);
  }

  async append(item, data) {
    return Promise.resolve(await this.tree.create(item, data));
  }

  async destroy() {
    const model = this.tree.getModel();
    this.rootNode.children = [];
    if (model.root) {
      model.root.isExpanded = false;
      return Promise.resolve(await model.root.hardReloadChildren());
    }
  }

  next(item) {
    if (item) {
      let parent = this.parent(item);
      if (parent && parent.children.length > 0) {
        let idx = parent.children.indexOf(item);
        if (idx !== -1 && parent.children.length !== idx + 1) {
          return parent.children[idx + 1];
        }
      }
    }
    return null;
  }

  prev(item) {
    if (item) {
      let parent = this.parent(item);
      if (parent && parent.children.length > 0) {
        let idx = parent.children.indexOf(item);
        if (idx !== -1 && idx !== 0) {
          return parent.children[idx - 1];
        }
      }
    }
    return null;
  }

  async open(item) {
    if (this.isOpen(item)) { return true; }
    await this.tree.toggleDirectory(item);
  }

  async ensureLoaded(item) {
    await item.ensureLoaded();
  }

  async ensureVisible(item, align='auto') {
    await this.tree.ensureVisible(item, align);
  }

  async openPath(item) {
    parent = item.parent;
    await this.tree.openDirectory(parent);
  }

  async close(item) {
    await this.tree.closeDir(item);
  }

  async toggle(item) {
    await this.tree.toggleDirectory(item);
  }

  async select(item, ensureVisible = false, align = 'auto') {
    await this.tree.setActiveFile(item, ensureVisible, align);
  }

  async selectNode(item, ensureVisible = false, align = 'auto') {
    this.tree.setActiveFile(item, ensureVisible, align);
  }

  async unload(item) {
    await this.tree.unload(item);
  }

  async addIcon(item, icon) {
    if (item?.getMetadata('data') !== undefined) {
      item.getMetadata('data').icon = icon.icon;
    }
    await this.tree.addIcon(item, icon);
  }

  removeIcon() {
    // TBD
  }

  setLeaf() {
    // TBD
  }
  async setLabel(item, label) {
    if (item) {
      await this.tree.setLabel(item, label);
    }
  }

  async setInode(item) {
    if (item._children) item._children = null;
    await this.tree.closeDirectory(item);
  }

  async setId(item, data) {
    if (item) {
      item.getMetadata('data').id = data.id;
    }
  }

  async deselect(item) {
    await this.tree.deSelectActiveFile(item);
  }

  wasInit() {
    // TBD
    return true;
  }

  wasLoad(item) {
    if (item?.type === FileType.Directory) {
      return item.isExpanded && item.children != null && item.children.length > 0;
    }
    return true;
  }

  parent(item) {
    return item.parent;
  }

  first(item) {
    const model = this.tree.getModel();
    if ((item === undefined || item === null) && model.root.children !== null) {
      return model.root.children[0];
    }

    if (item?.branchSize > 0) {
      return item.children[0];
    }

    return null;
  }

  children(item) {
    const model = this.tree.getModel();
    if (item) {
      return (item.children !== null ? item.children : []);
    }
    return model.root.children;
  }

  itemFrom(domElem) {
    return this.tree.getItemFromDOM(domElem);
  }

  DOMFrom(item) {
    return this.tree.getDOMFromItem(item);
  }

  addCssClass(item, cssClass) {
    this.tree.addCssClass(item, cssClass);
  }

  path(item) {
    if (item) return item.path;
  }

  pathId(item) {
    if (item) {
      let pathIds = item.path.split('/');
      pathIds.splice(0, 1);
      return pathIds;
    }
    return [];
  }

  itemFromDOM(domElem) {
    return this.tree.getItemFromDOM(domElem[0]);
  }

  siblings(item) {
    if (this.hasParent(item)) {
      let _siblings = this.parent(item).children.filter((_item) => _item.path !== item.path);
      if (typeof (_siblings) !== 'object') return [_siblings];
      else return _siblings;
    }
    return [];
  }

  hasParent(item) {
    return item?.parent;
  }

  isOpen(item) {
    if (item.type === FileType.Directory) {
      return item.isExpanded;
    }
    return false;
  }

  isClosed(item) {
    if (item.type === FileType.Directory) {
      return !item.isExpanded;
    }
    return false;
  }

  itemData(item) {
    return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];
  }

  getData(item) {
    return (item?.getMetadata('data') !== undefined) ? item?._metadata.data : [];
  }

  isRootNode(item) {
    const model = this.tree.getModel();
    return item === model.root;
  }

  isInode(item) {
    const children = this.children(item);
    if (children === null || children === undefined) return false;
    return children.length > 0;
  }

  selected() {
    return this.tree.getActiveFile();
  }

  resizeTree() {
    this.tree.resize();
  }

  findNodeWithToggle(path) {
    let tree = this;

    if (path == null || !Array.isArray(path)) {
      return Promise.reject(new Error(null));
    }
    const basepath = '/browser/' + path.slice(0, path.length-1).join('/') + '/';
    path = '/browser/' + path.join('/');

    let onCorrectPath = function (matchPath) {
      return (matchPath !== undefined && path !== undefined
        && (basepath.startsWith(`${matchPath}/`) || path === matchPath));
    };

    return (function findInNode(currentNode) {
      return new Promise((resolve, reject) => {
        if (path === null || path === undefined || path.length === 0) {
          resolve(null);
        }
        /* No point in checking the children if
         * the path for currentNode itself is not matching
         */
        if (currentNode.path !== undefined && !onCorrectPath(currentNode.path)) {
          reject(new Error(null));
        } else if (currentNode.path === path) {
          resolve(currentNode);
        } else {
          tree.open(currentNode)
            .then(() => {
              let children = currentNode.children;
              for (let i = 0, length = children.length; i < length; i++) {
                let childNode = children[i];
                if (onCorrectPath(childNode.path)) {
                  resolve(findInNode(childNode));
                  return;
                }
              }
              reject(new Error(null));
            })
            .catch(() => {
              reject(new Error(null));
            });
        }
      });
    })(tree.tree.getModel().root);
  }

  getNodeDisplayPath(item, separator='/', skip_coll=false) {
    let retStack = [];
    let currItem = item;
    while(currItem?.fileName) {
      const data = currItem._metadata?.data;
      if(data._type.startsWith('coll-') && skip_coll) {
        /* Skip collection */
      } else {
        retStack.push(data._label);
      }
      currItem = currItem.parent;
    }
    retStack = retStack.reverse();
    if(!separator) return retStack;
    return retStack.join(separator);
  }

  findNodeByDomElement(domElement) {
    const path = domElement?.path;
    if (!path?.[0]) {
      return undefined;
    }

    return this.findNode(path);
  }

  addNewNode(id, data, item, parentPath) {
    let parent;
    parent = this.findNode(parentPath);
    return this.createOrUpdateNode(id, data, parent, item);
  }

  findNode(path) {
    if (path === null || path === undefined || path.length === 0 || path == '/browser') {
      return this.rootNode;
    }
    return findInTree(this.rootNode, path);
  }

  createOrUpdateNode(id, data, parent, domNode) {
    let oldNodePath = id;
    if (parent?.path != '/browser') {
      oldNodePath = parent.path + '/' + id;
    }
    const oldNode = this.findNode(oldNodePath);
    if (oldNode !== null) {
      oldNode.data = data;
      oldNode.domNode = domNode;
      return oldNode;
    }

    const node = new TreeNode(id, data, domNode, parent);
    if (parent === this.rootNode) {
      node.parentNode = null;
    }

    if (parent !== null && parent !== undefined)
      parent.children.push(node);
    return node;
  }

  async updateAndReselectNode(item, data) {
    await this.update(item, data);
    await this.deselect(item);
    await this.select(item);
  }

  translateTreeNodeIdFromReactTree(treeNode) {
    let currentTreeNode = treeNode;
    let path = [];
    while (currentTreeNode !== null && currentTreeNode !== undefined) {
      if (currentTreeNode.path !== '/browser') path.unshift(currentTreeNode.path);
      if (this.hasParent(currentTreeNode)) {
        currentTreeNode = this.parent(currentTreeNode);
      } else {
        break;
      }
    }
    return path;
  }

  getTreeNodeHierarchy(identifier) {
    let idx = 0;
    let node_cnt = 0;
    let result = {};
    if (!identifier) return;
    let item;
    try {
      item = TreeNode.prototype.isPrototypeOf(identifier) ? identifier : this.findNode(identifier.path);
    } catch {
      // It is possible that the requested path is not present in the tree anymore
      item = null;
    }
    if (!item) return;
    do {
      const currentNodeData = item.getData();
      if (currentNodeData._type in this.Nodes && this.Nodes[currentNodeData._type].hasId) {
        const nodeType = mapType(currentNodeData._type, node_cnt);
        if (result[nodeType] === undefined) {
          result[nodeType] = _.extend({}, currentNodeData, {
            'priority': idx,
          });
          idx -= 1;
        }
      }
      node_cnt += 1;
      item = item.hasParent() ? item.parent() : null;
    } while (item);

    return result;
  }

  /*
   *
   * The dropDetailsFunc should return an object of sample
   * {text: 'xyz', cur: {from:0, to:0} where text is the drop text and
   * cur is selection range of text after dropping. If returned as
   * string, by default cursor will be set to the end of text
   */
  registerDraggableType(typeOrTypeDict, dropDetailsFunc = null) {
    if (typeof typeOrTypeDict == 'object') {
      Object.keys(typeOrTypeDict).forEach((type) => {
        this.registerDraggableType(type, typeOrTypeDict[type]);
      });
    } else if (dropDetailsFunc != null) {
      typeOrTypeDict.replace(/ +/, ' ').split(' ').forEach((type) => {
        this.draggableTypes[type] = dropDetailsFunc;
      });
    }
  }

  getDraggable(type) {
    if (this.draggableTypes[type]) {
      return this.draggableTypes[type];
    } else {
      return null;
    }
  }

  handleDraggable(e, item) {
    let data = item.getMetadata('data');
    let dropDetailsFunc = this.getDraggable(data._type);

    if (dropDetailsFunc != null) {
      let dropDetails = dropDetailsFunc(data, item, this.getTreeNodeHierarchy(item));

      if (typeof dropDetails == 'string') {
        dropDetails = {
          text: dropDetails,
          cur: {
            from: dropDetails.length,
            to: dropDetails.length,
          },
        };
      } else if (!dropDetails.cur) {
        dropDetails = {
          ...dropDetails,
          cur: {
            from: dropDetails.text.length,
            to: dropDetails.text.length,
          },
        };
      }

      e.dataTransfer.setData('text', JSON.stringify(dropDetails));
      /* Required by Firefox */
      if (e.dataTransfer.dropEffect) {
        e.dataTransfer.dropEffect = 'move';
      }

      /* setDragImage is not supported in IE. We leave it to
      * its default look and feel
      */
      const dropText = _.escape(dropDetails.text);
      if(!dropText) {
        e.preventDefault();
      }
      if (e.dataTransfer.setDragImage) {
        const dragItem = document.createElement('div');
        dragItem.classList.add('drag-tree-node');
        dragItem.innerHTML = `<span>${dropText}</span>`;

        document.querySelector('body .drag-tree-node')?.remove();
        document.body.appendChild(dragItem);

        e.dataTransfer.setDragImage(dragItem, 0, 0);
      }
    }
    else {
      e.preventDefault();
    }
  }

  onNodeCopy(copyCallback) {
    this.copyHandler = copyCallback;
  }

  toggleItemLoader(item, show) {
    this.tree.toggleItemLoader(item, show);
  }
}

function mapType(type, idx) {
  return (type === 'partition' && idx > 0) ? 'table' : type;
}



/**
 * Given an initial node and a path, it will navigate through
 * the new tree to find the node that matches the path
 */
export function findInTree(rootNode, path) {
  if (path === null) {
    return rootNode;
  }
  return (function findInNode(currentNode) {

    /* No point in checking the children if
     * the path for currentNode itself is not matching
     */
    if (currentNode.path !== undefined && path !== undefined
      && !path.startsWith(currentNode.path)) {
      return null;
    }

    for (let i = 0, length = currentNode.children.length; i < length; i++) {
      const calculatedNode = findInNode(currentNode.children[i]);
      if (calculatedNode !== null) {
        return calculatedNode;
      }
    }

    if (currentNode.path === path) {
      return currentNode;
    } else {
      return null;
    }
  })(rootNode);
}

const isValidTreeNodeData = (data) => (!_.isEmpty(data));

export { isValidTreeNodeData };
