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

import _ from 'lodash';
import getApiInstance from '../../../static/js/api_instance';
import { AllPermissionTypes, BROWSER_PANELS } from './constants';
import ObjectNodeProperties from '../../../misc/properties/ObjectNodeProperties';
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
import toPx from '../../../static/js/to_px';
import usePreferences from '../../../preferences/static/js/store';
import { evalFunc } from '../../../static/js/utils';

define('pgadmin.browser.node', [
  'sources/gettext', 'sources/pgadmin',
  'sources/browser/generate_url', 'sources/utils',
  'pgadmin.browser.utils', 'pgadmin.browser.events',
], function(
  gettext, pgAdmin, generateUrl, commonUtils
) {
  const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};

  // It has already been defined.
  // Avoid running this script again.
  if (pgBrowser.Node)
    return pgBrowser.Node;

  pgBrowser.Nodes = pgBrowser.Nodes || {};

  // A helper (base) class for all the nodes, this has basic
  // operations/callbacks defined for basic operation.
  pgBrowser.Node = function() {/*This is intentional (SonarQube)*/};

  // Helper function to correctly set up the property chain, for subclasses.
  // Uses a hash of class properties to be extended.
  //
  // It is unlikely - we will instantiate an object for this class.
  pgBrowser.Node.extend = function(props, initialize) {
    let parent = this;
    let child;

    // The constructor function for the new subclass is defined to simply call
    // the parent's constructor.
    child = function() {
      return parent.apply(this, arguments);
    };

    // Add static properties to the constructor function, if supplied.
    _.extend(child, parent, _.omit(props, 'callbacks'));

    // Make sure - a child have all the callbacks of the parent.
    child.callbacks = _.extend({}, parent.callbacks, props.callbacks);

    // Let's not bind the callbacks, or initialize the child.
    if (!(initialize??true))
      return child;

    let bindToChild = function(cb) {
        if (typeof(child.callbacks[cb]) == 'function') {
          child.callbacks[cb] = child.callbacks[cb].bind(child);
        }
      },
      callbacks = _.keys(child.callbacks);
    for (let cb_val of callbacks) bindToChild(cb_val);

    // Registering the node by calling child.Init(...) function
    child.Init.apply(child);

    // Initialize the parent
    this.Init.apply(child);

    return child;
  };

  _.extend(pgAdmin.Browser.Node, {
    // Node type
    type: undefined,
    // Label
    label: '',
    // Help pages
    sqlAlterHelp: '',
    sqlCreateHelp: '',
    dialogHelp: '',
    epasHelp: false,

    title: function(d, action) {
      if(action == 'create') {
        return gettext('Create - %s', this.label);
      }
      return d._label??'';
    },
    copy: function(d) {
      // This function serves the purpose of facilitating modifications
      // during the copying process of any node.
      return d;
    },
    getNodeInfoLabel: function(data) {
      // Adds an extra informational label appended to the node label.
      // Override this function to return a string to display, or null for no extra label.
      if(data.info_label) 
        return data.info_label;
      else
        return null;
    },
    hasId: true,
    ///////
    // Initialization function
    // Generally - used to register the menus for this type of node.
    //
    // Also, look at pgAdmin.Browser.add_menus(...) function.
    //
    // NOTE: Override this for each node for initialization purpose
    Init: function() {
      let self = this;
      if (self.node_initialized)
        return;
      self.node_initialized = true;

      pgAdmin.Browser.add_menus([{
        name: 'refresh',
        node: self.type,
        module: self,
        applies: ['object', 'context'],
        callback: 'refresh',
        priority: 2,
        label: gettext('Refresh...'),
        shortcut_preference: ['browser', 'sub_menu_refresh'],
        enable: true,
      }]);

      if (self.canEdit) {
        pgAdmin.Browser.add_menus([{
          name: 'show_obj_properties',
          node: self.type,
          module: self,
          applies: ['object', 'context'],
          callback: 'show_obj_properties',
          priority: 999,
          label: gettext('Properties...'),
          data: {
            'action': 'edit',
          },
          shortcut_preference: ['browser', 'sub_menu_properties'],
          enable: _.isFunction(self.canEdit) ?
            function() {
              return !!(self.canEdit(...arguments));
            } : (!!self.canEdit),
          permission: self.type == 'server' ? AllPermissionTypes.OBJECT_REGISTER_SERVER : undefined,
        }]);
      }

      if (self.canDrop) {
        pgAdmin.Browser.add_menus([{
          name: 'delete_object',
          node: self.type,
          module: self,
          applies: ['object', 'context'],
          callback: 'delete_obj',
          priority: self.dropPriority,
          label: (self.dropAsRemove) ? gettext('Remove %s', self.label) : gettext('Drop'),
          data: {
            'url': 'drop',
            data_disabled: gettext('The selected tree node does not support this option.'),
          },
          shortcut_preference: ['browser', 'sub_menu_delete'],
          enable: _.isFunction(self.canDrop) ?
            function() {
              return !!(self.canDrop(...arguments));
            } : (!!self.canDrop),
          permission: self.type == 'server' ? AllPermissionTypes.OBJECT_REGISTER_SERVER : undefined,
        }]);

        if (self.canDropCascade) {
          pgAdmin.Browser.add_menus([{
            name: 'delete_object_cascade',
            node: self.type,
            module: self,
            applies: ['object', 'context'],
            callback: 'delete_obj',
            priority: 2,
            label: gettext('Drop (Cascade)'),
            data: {
              'url': 'delete',
            },
            enable: _.isFunction(self.canDropCascade) ?
              function() {
                return self.canDropCascade(...arguments);
              } : (!!self.canDropCascade),
          }]);
        }
      }

      // Show query tool only in context menu of supported nodes.
      if (_.indexOf(pgAdmin.unsupported_nodes, self.type) == -1) {
        let enable = function(itemData) {
          if (itemData?._type == 'database' && itemData?.allowConn)
            return true;
          else if (itemData?._type != 'database')
            return true;
          else
            return false;
        };
        pgAdmin.Browser.add_menus([{
          name: 'show_query_tool',
          node: self.type,
          module: self,
          applies: ['context'],
          callback: 'show_query_tool',
          priority: 998,
          label: gettext('Query Tool'),
          enable: enable,
          permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
          shortcut_preference: ['browser', 'sub_menu_query_tool'],
        }]);

        // show search objects same as query tool
        pgAdmin.Browser.add_menus([{
          name: 'search_objects', node: self.type, module: pgAdmin.Tools.SearchObjects,
          applies: ['context'], callback: 'show_search_objects',
          priority: 997, label: gettext('Search Objects...'),
          icon: 'fa fa-search', enable: enable,
          permission: AllPermissionTypes.TOOLS_SEARCH_OBJECTS,
          shortcut_preference: ['browser', 'sub_menu_search_objects'],
        }]);

        if(pgAdmin['enable_psql']) {
          // show psql tool same as query tool.
          pgAdmin.Browser.add_menus([{
            name: 'show_psql_tool', node: this.type, module: this,
            applies: ['context'], callback: 'show_psql_tool',
            priority: 998, label: gettext('PSQL Tool'),
            permission: AllPermissionTypes.TOOLS_PSQL_TOOL,
          }]);
        }
      }

      // This will add options of scripts eg:'CREATE Script'
      if (self.hasScriptTypes && _.isArray(self.hasScriptTypes) &&
        self.hasScriptTypes.length > 0) {
        // For each script type create menu
        _.each(self.hasScriptTypes, function(stype) {

          let type_label = gettext('%s Script',stype.toUpperCase());

          stype = stype.toLowerCase();

          // Adding menu for each script type
          pgAdmin.Browser.add_menus([{
            name: 'show_script_' + stype,
            node: self.type,
            module: self,
            applies: ['object', 'context'],
            callback: 'show_script',
            priority: 4,
            label: type_label,
            category: gettext('Scripts'),
            data: {
              'script': stype,
              data_disabled: gettext('The selected tree node does not support this option.'),
            },
            enable: self.check_user_permission,
            permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
          }]);
        });
      }
    },
    ///////
    // Checks if Script Type is allowed to user
    // First check if role node & create role allowed
    // Otherwise test rest of database objects
    // if no permission matched then do not allow create script
    ///////
    check_user_permission: function(itemData, item, data) {
      // Do not display CREATE script on server group and server node
      if (itemData._type == 'server_group' || itemData._type == 'server') {
        return false;
      }

      // Do not display the menu if the database connection is not allowed
      if (itemData._type == 'database' && !itemData.allowConn)
        return false;

      let parentData = pgBrowser.tree.getTreeNodeHierarchy(item);
      if (_.indexOf(['create', 'insert', 'update', 'delete'], data.script) != -1) {
        if (itemData.type == 'role' &&
          parentData.server.user.can_create_role) {
          return true;
        } else if (
          ( parentData.server?.user.is_superuser || parentData.server?.user.can_create_db) ||
          (parentData.schema?.can_create)
        ) {
          return true;
        } else {
          return false;
        }
      } else {
        return true;
      }
    },
    /*
     * Default script type menu for node.
     *
     * Override this, to show more script type menus (e.g hasScriptTypes: ['create', 'select', 'insert', 'update', 'delete'])
     *
     * Or set it to empty array to disable script type menu on node (e.g hasScriptTypes: [])
     */
    hasScriptTypes: ['create'],
    /******************************************************************
     * This function determines the given item is editable or not.
     *
     * Override this, when a node is not editable.
     */
    canEdit: true,
    /******************************************************************
     * This function determines the given item is deletable or not.
     *
     * Override this, when a node is not deletable.
     */
    canDrop: false,
    /************************************************************************
     * This function determines the given item and children are deletable or
     * not.
     *
     * Override this, when a node is not deletable.
     */
    canDropCascade: false,
    /*********************************************************************************
    dropAsRemove should be true in case, Drop object label needs to be replaced by Remove
    */
    dropAsRemove: false,
    /******************************************************************************
    dropPriority is set to 2 by default, override it when change is required
    */
    dropPriority: 2,
    /******************************************************************************
    select collection node on deletion.
    */
    selectParentNodeOnDelete: false,
    // List of common callbacks - that can be used for different
    // operations!
    callbacks: {
      /******************************************************************
       * This function allows to create/edit/show properties of any
       * object depending on the arguments provided.
       *
       * args must be a object containing:
       *   action - create/edit/properties
       *   item   - The properties of the item (tree node item)
       *
       * NOTE:
       * if item is not provided, the action will be done on the
       * currently selected tree item node.
       *
       **/
      show_obj_properties: function(args, item) {
        let t = pgBrowser.tree,
          nodeItem = args?.item || item || t.selected(),
          nodeData = nodeItem ? t.itemData(nodeItem) : undefined,
          panelTitle = this.title(nodeData, args.action),
          treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);

        if (!nodeData)
          return;

        const isParent = (_.isArray(this.parent_type) ?
          (_d)=>{
            return (_.indexOf(this.parent_type, _d._type) != -1);
          } : (_d)=>{
            return (this.parent_type == _d._type);
          });

        if (args.action == 'create') {
          // If we've parent, we will get the information of it for
          // proper object manipulation.
          if (this.parent_type && !isParent(nodeData)) {
            // actual parent of a table is schema, not Tables.
            while (nodeItem && t.hasParent(nodeItem)) {
              nodeItem = t.parent(nodeItem);
              let pd = t.itemData(nodeItem);

              if (isParent(pd)) {
                // Assign the data, this is my actual parent.
                nodeData = pd;
                break;
              }
            }
          }

          // The only node who does not have parent is the Server Group
          if (!nodeData || (this.parent_type != null && !isParent(nodeData))) {
            // It should never come here.
            // If it is here, that means - we do have some bug in code.
            return;
          }

          treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
          const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
          const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
          const onSave = (newNodeData)=>{
            // Clear the cache for this node now.
            setTimeout(()=>{
              this.clear_cache(item);
            }, 0);
            try {
              pgBrowser.Events.trigger(
                'pgadmin:browser:tree:add', _.clone(newNodeData.node),
                _.clone(treeNodeInfo)
              );
            } catch (e) {
              console.warn(e.stack || e);
            }
            onClose();
          };
          this.showPropertiesDialog(panelId, panelTitle, {
            treeNodeInfo: treeNodeInfo,
            item: nodeItem,
            nodeData: nodeData,
            actionType: 'create',
            onSave: onSave,
            onClose: onClose,
          });
        } else if (args.action == 'copy') {
          // This else-if block is used to copy the existing object and
          // open the respective dialog. Add the copied object into the object
          // browser tree upon the 'Save' button click.
          treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(nodeItem);
          const panelId = _.uniqueId(BROWSER_PANELS.EDIT_PROPERTIES);
          const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
          const onSave = (newNodeData)=>{
            // Clear the cache for this node now.
            setTimeout(()=>{
              this.clear_cache(item);
            }, 0);
            try {
              pgBrowser.Events.trigger(
                'pgadmin:browser:tree:add', _.clone(newNodeData.node),
                {'server_group': treeNodeInfo['server_group']}
              );
            } catch (e) {
              console.warn(e.stack || e);
            }
            onClose();
          };
          this.showPropertiesDialog(panelId, panelTitle, {
            treeNodeInfo: treeNodeInfo,
            item: nodeItem,
            nodeData: nodeData,
            actionType: 'copy',
            onSave: onSave,
            onClose: onClose,
          });
        } else {
          const panelId = BROWSER_PANELS.EDIT_PROPERTIES+nodeData.id;
          const onClose = (force=false)=>{ pgBrowser.docker.default_workspace.close(panelId, force); };
          const onSave = (newNodeData)=>{
            let _old = nodeData,
              _new = newNodeData.node,
              info = treeNodeInfo;

            // Clear the cache for this node now.
            setTimeout(()=>{
              this.clear_cache(item);
            }, 0);

            pgBrowser.Events.trigger(
              'pgadmin:browser:tree:update',
              _old, _new, info, {
                success: function(_item, _newNodeData, _oldNodeData) {
                  pgBrowser.Events.trigger(
                    'pgadmin:browser:node:updated', _item, _newNodeData,
                    _oldNodeData
                  );
                  pgBrowser.Events.trigger(
                    'pgadmin:browser:node:' + _newNodeData._type + ':updated',
                    _item, _newNodeData, _oldNodeData
                  );

                  // Trigger a specific event for server color updates
                  // This allows tabs to update their color indicators when server colors change
                  if (_newNodeData._type === 'server') {
                    // Extract colors from the icon string using getServerColors utility
                    const newColors = commonUtils.getServerColors(_newNodeData?.icon);

                    pgBrowser.Events.trigger(
                      'pgadmin:server:colors:updated',
                      _newNodeData._id,
                      {
                        bgcolor: newColors.bgcolor,
                        fgcolor: newColors.fgcolor,
                        icon: _newNodeData.icon
                      }
                    );
                  }
                },
              }
            );
            onClose();
          };
          if(pgBrowser.docker.default_workspace.find(panelId)) {
            let msg = gettext('Are you sure want to stop editing the properties of %s "%s"?');
            if (args.action == 'edit') {
              msg = gettext('Are you sure want to reset the current changes and re-open the panel for %s "%s"?');
            }

            pgAdmin.Browser.notifier.confirm(
              gettext('Edit in progress?'),
              commonUtils.sprintf(msg, this.label.toLowerCase(), nodeData.label),
              ()=>{
                this.showPropertiesDialog(panelId, panelTitle, {
                  treeNodeInfo: treeNodeInfo,
                  item: nodeItem,
                  nodeData: nodeData,
                  actionType: 'edit',
                  onSave: onSave,
                  onClose: onClose,
                }, true);
              },
              null
            );
          } else {
            this.showPropertiesDialog(panelId, panelTitle, {
              treeNodeInfo: treeNodeInfo,
              item: nodeItem,
              nodeData: nodeData,
              actionType: 'edit',
              onSave: onSave,
              onClose: onClose,
            });
          }
        }
      },
      // Delete the selected object
      delete_obj: function(args, item) {
        let input = args || {
            'url': 'drop',
          },
          obj = this,
          t = pgBrowser.tree,
          i = input.item || item || t.selected(),
          d = i ? t.itemData(i) : undefined;

        if (!d)
          return;

        /*
         * Make sure - we're using the correct version of node
         */
        obj = pgBrowser.Nodes[d._type];
        let objName = _.unescape(d.label);

        let msg, title;

        if (input.url == 'delete' && d._type === 'database') {
          msg = gettext('Drop database with the force option will attempt to terminate all existing connections to the <b>"%s"</b> database. Are you sure you want to proceed?', d.label);
          title = gettext('Drop FORCE %s?', obj.label);

        } else if (input.url == 'delete') {
          msg = gettext('Are you sure you want to drop the %s <b>"%s"</b> and all the objects that depend on it?',
            obj.label.toLowerCase(), d.label);
          title = gettext('Drop CASCADE %s?', obj.label);

          if (!(_.isFunction(obj.canDropCascade) ?
            obj.canDropCascade(d, i) : obj.canDropCascade)) {
            pgAdmin.Browser.notifier.errorText(
              gettext('The %s "%s" cannot be dropped.', obj.label, d.label),
              10000
            );
            return;
          }
        } else {
          if (obj.dropAsRemove) {
            msg = gettext('Are you sure you want to remove the %s <b>"%s"</b>?', obj.label.toLowerCase(), d.label);
            title = gettext('Remove %s?', obj.label);
          } else {
            msg = gettext('Are you sure you want to drop the %s <b>"%s"</b>?', obj.label.toLowerCase(), d.label);
            title = gettext('Drop %s?', obj.label);
          }

          if (!(_.isFunction(obj.canDrop) ?
            obj.canDrop(d, i) : obj.canDrop)) {
            pgAdmin.Browser.notifier.errorText(
              gettext('The %s "%s" cannot be dropped/removed.', obj.label, d.label),
              10000
            );
            return;
          }
        }
        pgAdmin.Browser.notifier.confirmDelete(title, msg,
          function() {
            getApiInstance().delete(
              obj.generate_url(i, input.url, d, true),
            ).then(({data: res})=> {
              if(res.success == 2){
                pgAdmin.Browser.notifier.errorText(res.info, null);
                return;
              }
              if (res.success == 0) {
                pgAdmin.Browser.notifier.alertText(res.errormsg, res.info);
              } else {
                // Remove the node from tree and set collection node as selected.
                let selectNextNode = true;
                if(obj.selectParentNodeOnDelete) {
                  let prv_i = t.parent(i);
                  setTimeout(function() {
                    t.select(prv_i);
                  }, 10);
                  selectNextNode = false;
                }
                if (obj.refreshParent?.(i)) {
                  obj.callbacks.refresh('refresh', t.parent(i));
                  return;
                }

                pgBrowser.removeTreeNode(i, selectNextNode);
              }
              return true;
            }).catch(function(error) {
              let errmsg = error.request?.responseText;
              /* Error from the server */
              if (error.request?.status == 417 || error.request?.status == 410 || error.request?.status == 500) {
                try {
                  let data = error.response.data;
                  errmsg = data.info || data.errormsg;
                } catch (e) {
                  console.warn(e.stack || e);
                }
              }
              pgAdmin.Browser.notifier.alertText(gettext('Error dropping/removing %s: "%s"', obj.label, objName), errmsg);
            });
          },
          () => {},
          obj.dropAsRemove ? gettext('Remove') : gettext('Drop'),
          gettext('Cancel'),
        );
      },
      // Callback for creating script(s) & opening them in Query editor
      show_script: function(args, item) {
        let scriptType = args.script,
          obj,
          t = pgBrowser.tree,
          i = item || t.selected(),
          d = i ? t.itemData(i) : undefined;

        if (!d)
          return;

        /*
         * Make sure - we're using the correct version of node
         */
        obj = pgBrowser.Nodes[d._type];
        let sql_url;

        // URL for script type
        if (scriptType == 'insert') {
          sql_url = 'insert_sql';
        } else if (scriptType == 'update') {
          sql_url = 'update_sql';
        } else if (scriptType == 'delete') {
          sql_url = 'delete_sql';
        } else if (scriptType == 'select') {
          sql_url = 'select_sql';
        } else if (scriptType == 'exec') {
          sql_url = 'exec_sql';
        } else {
          // By Default get CREATE SQL
          sql_url = 'sql';
        }
        // Open data grid & pass the URL for fetching
        pgAdmin.Tools.SQLEditor.showQueryTool(
          obj.generate_url(i, sql_url, d, true),
          i, scriptType
        );
      },

      // Callback to render query editor
      show_query_tool: function(args, item) {
        let preference = usePreferences.getState().getPreferences('sqleditor', 'copy_sql_to_query_tool');
        let t = pgBrowser.tree,
          i = item || t.selected(),
          d = i ? t.itemData(i) : undefined;

        if (!d)
          return;

        // Here call data grid method to render query tool
        //Open query tool with create script if copy_sql_to_query_tool is true else open blank query tool
        if(preference.value && !d._type.includes('coll-')){
          let stype = d._type.toLowerCase();
          let data = {
            'script': stype,
            data_disabled: gettext('The selected tree node does not support this option.'),
          };
          pgBrowser.Node.callbacks.show_script(data);
        }else{
          pgAdmin.Tools.SQLEditor.showQueryTool('', i);
        }
      },

      // Callback to render psql tool.
      show_psql_tool: function(args) {
        let input = args || {},
          t = pgBrowser.tree,
          i = input.item || t.selected(),
          d = i  ? t.itemData(i) : undefined;
        pgAdmin.Tools.Psql.openPsqlTool(d, i);
      },

      // Logic to change the server background colour
      // There is no way of applying CSS to parent element so we have to
      // do it via JS code only
      change_server_background: function(item, data) {
        if (!item || !data)
          return;
        const treeH = pgBrowser.tree.getTreeNodeHierarchy(item);
        const serverData = treeH['server'];
        if (!serverData) {
          return;
        }
        const index = item.path.indexOf(serverData.id);

        // Go further only if node type is a Server
        if (index !== -1) {
          // Extract bgcolor and fgcolor from server icon
          const { bgcolor, fgcolor } = commonUtils.getServerColors(serverData.icon);

          if (bgcolor) {
            let dynamic_class = 'pga_server_' + serverData._id + '_bgcolor';
            // Prepare dynamic style tag
            const styleTag = document.createElement('style');
            styleTag.setAttribute('id', dynamic_class);
            styleTag.setAttribute('type', 'text/css');
            styleTag.innerText = `
              .${dynamic_class} .file-label {
                border-radius: 3px;
                margin-bottom: 2px;
                background: ${bgcolor} !important;
              }
              ${fgcolor ? `
              .${dynamic_class} span.file-name, .${dynamic_class} span.file-name:hover, .${dynamic_class} span.file-name.pseudo-active,
              .${dynamic_class} span.children-count {
                color: ${fgcolor} !important;
              }
              .${dynamic_class} span.text-muted {
                color: color-mix(in srgb, ${fgcolor} 65%, transparent) !important;
              }
              `:''}
            `;

            // Prepare dynamic style tag using template
            document.querySelector(`style[id="${dynamic_class}"]`)?.remove();
            document.head.appendChild(styleTag);
            // Add dynamic class to the tree node.
            pgBrowser.tree.addCssClass(item, dynamic_class);
          }
        }
      },
      added: function(item, data) {
        if (pgBrowser.tree.getData(item)._type.indexOf('coll-') !== -1){
          setTimeout(function() {
            let _item = pgAdmin.Browser.Nodes[pgBrowser.tree.getData(item).nodes[0]];
            _item.clear_cache.apply(_item);
          }, 0);
        }
        pgBrowser.Node.callbacks.change_server_background(item, data);
        // Suppress added tree event being called during object search operations
        // where tree.select clashes due to previous tree state restore
        const suppressPath = pgBrowser.tree.suppressEventsForPath;
        if (suppressPath) {
          if (item.path === suppressPath) {
            pgBrowser.tree.suppressEventsForPath = null;
          } else {
            return;
          }
        }

        pgBrowser.Events.trigger('pgadmin:browser:tree:expand-from-previous-tree-state', item);
      },
      // Callback called - when a node is selected in browser tree.
      selected: function(item, data) {
        // Show the information about the selected node in the below panels,
        // which are visible at this time:
        // + Properties
        // + Query (if applicable, otherwise empty)
        // + Dependents
        // + Dependencies
        // + Statistics
        // Update the menu items
        pgAdmin.Browser.enable_disable_menus.apply(pgBrowser, [item]);

        pgBrowser.Events.trigger('pgadmin-browser:node:selected', item, data);

        pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state',
          item);
        return true;
      },
      // Callback called - when a node is deselected in browser tree.
      deselected: function() {
        // The following call disables all menus mapped to any selected tree node.
        pgAdmin.Browser.enable_disable_menus.apply(pgBrowser, []);
      },
      removed: function(item) {
        let self = this;
        setTimeout(function() {
          self.clear_cache(item);
        }, 0);
      },
      refresh: function(cmd, _item) {
        let self = this,
          t = pgBrowser.tree,
          data = _item && t.itemData(_item);

        pgBrowser.Events.trigger(
          'pgadmin:browser:tree:refresh', _item || pgBrowser.tree.selected(), {
            success: function() {
              self.callbacks.selected.apply(self, [_item, data, pgBrowser]);
            },
          });
      },
      opened: function(item) {
        let tree = pgBrowser.tree,
          auto_expand = usePreferences.getState().getPreferences('browser', 'auto_expand_sole_children');
        // Suppress opened tree event being called during object search operations
        // where tree.select clashes due to only child of parent opens automatically.
        const suppressPath = pgBrowser.tree.suppressEventsForPath;
        if (suppressPath) {
          if (item.path === suppressPath) {
            pgBrowser.tree.suppressEventsForPath = null;
          } else {
            return;
          }
        }

        if (auto_expand?.value && tree.children(item).length == 1) {
          // Automatically expand the child node, if a treeview node has only a single child.
          const first_child = tree.first(item);

          if (first_child._loaded) {
            tree.open(first_child);
            tree.select(first_child);
          } else {
            const openSoleItem = setInterval(() => {
              if (first_child._loaded) {
                tree.open(first_child);
                tree.select(first_child);
                clearSoleItemInterval();
              }
            }, 200);
            const clearSoleItemInterval = function() {
              clearInterval(openSoleItem);
            };
          }

        } else if(tree.children(item).length == 1) {
          const first_child = tree.first(item);
          tree.select(first_child);
        }

        pgBrowser.Events.trigger('pgadmin:browser:tree:update-tree-state', item);

      },
      closed: function(item) {
        pgBrowser.Events.trigger('pgadmin:browser:tree:remove-from-tree-state',
          item);
      },
    },
    showPropertiesDialog: function(panelId, panelTitle, dialogProps, update=false) {
      const panelData = {
        id: panelId,
        title: panelTitle,
        manualClose: true,
        icon: `dialog-node-icon ${evalFunc(this, this.node_image, dialogProps.nodeData) ?? ('icon-' + this.type)}`,
        content: (
          <ErrorBoundary>
            <ObjectNodeProperties
              panelId={panelId}
              node={this}
              formType="dialog"
              {...dialogProps}
            />
          </ErrorBoundary>
        )
      };

      let w = toPx(this.width || (pgBrowser.stdW.default + 'px'), 'width', true);
      let h = toPx(this.height || (pgBrowser.stdH.default + 'px'), 'height', true);

      /* Fit to standard sizes */
      if(w <= pgBrowser.stdW.sm) {
        w = pgBrowser.stdW.sm;
      } else if(w <= pgBrowser.stdW.md) {
        w = pgBrowser.stdW.md;
      } else {
        w = pgBrowser.stdW.lg;
      }

      if(h <= pgBrowser.stdH.sm) {
        h = pgBrowser.stdH.sm;
      } else if(h <= pgBrowser.stdH.md) {
        h = pgBrowser.stdH.md;
      } else {
        h = pgBrowser.stdH.lg;
      }

      if(update) {
        dialogProps.onClose(true);
        setTimeout(()=>{
          pgBrowser.docker.default_workspace.openDialog(panelData, w, h);
        }, 10);
      } else {
        pgBrowser.docker.default_workspace.openDialog(panelData, w, h);
      }
    },
    _find_parent_node: function(t, i, d) {
      if (this.parent_type) {
        d = d || t.itemData(i);

        if (_.isString(this.parent_type)) {
          if (this.parent_type == d._type) {
            return i;
          }
          while (t.hasParent(i)) {
            i = t.parent(i);
            d = t.itemData(i);

            if (this.parent_type == d._type)
              return i;
          }
        } else {
          if (_.indexOf(this.parent_type, d._type) >= 0) {
            return i;
          }
          while (t.hasParent(i)) {
            i = t.parent(i);
            d = t.itemData(i);

            if (_.indexOf(this.parent_type, d._type) >= 0)
              return i;
          }
        }
      }
      return null;
    },
    /**********************************************************************
     * Generate the URL for different operations
     *
     * arguments:
     *   type:  Create/drop/edit/properties/sql/depends/statistics
     *   d:     Provide the ItemData for the current item node
     *   with_id: Required id information at the end?
     *   jump_after_node: This will skip all the value between jump_after_node
     *   to the last node, excluding jump_after_node and the last node. This is particularly
     *   helpful in partition table where we need to skip parent table OID of a partitioned
     *   table in URL formation. Partitioned table itself is a "table" and can be multilevel
     * Supports url generation for create, drop, edit, properties, sql,
     * depends, statistics
     */
    generate_url: function(item, type, d, with_id, info, jump_after_node) {
      let opURL = {
          'create': 'obj',
          'drop': 'obj',
          'edit': 'obj',
          'properties': 'obj',
          'statistics': 'stats',
        },
        self = this,
        priority = -Infinity;
      let treeInfo = (_.isUndefined(item) || _.isNull(item)) ?
        info || {} : pgBrowser.tree.getTreeNodeHierarchy(item);
      let actionType = type in opURL ? opURL[type] : type;
      let itemID = with_id && d._type == self.type ? encodeURIComponent(d._id) : '';

      if (self.parent_type) {
        if (_.isString(self.parent_type)) {
          let p = treeInfo[self.parent_type];
          if (p) {
            priority = p.priority;
          }
        } else {
          _.each(self.parent_type, function(o) {
            let p = treeInfo[o];
            if (p) {
              if (priority < p.priority) {
                priority = p.priority;
              }
            }
          });
        }
      }

      let jump_after_priority = priority;
      if(jump_after_node && treeInfo[jump_after_node]) {
        jump_after_priority = treeInfo[jump_after_node].priority;
      }

      let nodePickFunction = function(treeInfoValue) {
        return (treeInfoValue.priority <= jump_after_priority || treeInfoValue.priority == priority);
      };

      return generateUrl.generate_url(pgBrowser.URL, treeInfo, actionType, self.type, nodePickFunction, itemID);
    },
    cache: function(url, node_info, level, data) {
      let cached = this.cached = this.cached || {},
        hash = url,
        min_priority = node_info?.[level]?.priority || 0;

      if (node_info) {
        _.each(_.sortBy(_.values(_.pickBy(
          node_info,
          function(v) {
            return (v.priority <= min_priority);
          }
        )), function(o) {
          return o.priority;
        }), function(o) {
          hash = commonUtils.sprintf('%s_%s', hash, encodeURI(o._id));
        });
      }

      if (_.isUndefined(data)) {
        let res = cached[hash];

        if (!_.isUndefined(res) &&
          (res.at - Date.now() > 300000)) {
          res = undefined;
        }
        return res;
      }

      let res = cached[hash] = {
        data: data,
        at: Date.now(),
        level: level,
      };
      return res;
    },
    clear_cache: function(item) {
      /*
       * Reset the cache, when new node is created.
       *
       * FIXME:
       * At the moment, we will clear all the cache for this node. But - we
       * would like to clear the cache only this nodes parent, so that - it
       * fetches the new data.
       */
      this.cached = {};

      // Trigger Notify event about node's cache
      let self = this;
      pgBrowser.Events.trigger(
        'pgadmin:browser:node:' + self.type + ':cache_cleared',
        item, self
      );
    },
    cache_level: function(node_info, with_id) {
      if (node_info) {
        if (with_id && this.type in node_info) {
          return this.type;
        }
        if (_.isArray(this.parent_type)) {
          for (let parent in this.parent_type) {
            if (parent in node_info) {
              return parent;
            }
          }
          return this.type;
        }
        return this.parent_type;
      }
    },
  });

  return pgAdmin.Browser.Node;
});
