/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package browser;

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.AbstractAction;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreeSelectionModel;

import ilog.cpl.IlpEquipment;
import ilog.cpl.IlpNetwork;
import ilog.cpl.IlpTree;
import ilog.cpl.datasource.IlpDataSource;
import ilog.cpl.datasource.IlpIdentifierFactory;
import ilog.cpl.datasource.IlpMutableDataSource;
import ilog.cpl.datasource.structure.IlpChild;
import ilog.cpl.datasource.structure.IlpContainer;
import ilog.cpl.equipment.IlpEquipmentSelectionModel;
import ilog.cpl.graph.background.IlpBackgroundHandlingException;
import ilog.cpl.interactor.IlpGesture;
import ilog.cpl.interactor.IlpViewActionEvent;
import ilog.cpl.interactor.IlpViewInteractor;
import ilog.cpl.model.IlpObject;
import ilog.cpl.service.IlpURLAccessService;
import ilog.cpl.storage.IlpDataSourceLoader;
import ilog.cpl.util.selection.IlpObjectSelectionModel;
// JTGO import statements
import ilog.tgo.model.IltCardItem;
import ilog.tgo.model.IltGroup;
import ilog.tgo.model.IltNetworkElement;
import ilog.tgo.model.IltObject;
import ilog.tgo.model.IltShelf;
import ilog.tgo.model.IltShelfItem;

/**
 */
public class DrillDownManager extends Object implements TreeSelectionListener {
  IlpTree treeComponent;
  IlpNetwork networkComponent;
  IlpEquipment equipmentComponent;
  IlpObject currentNetworkRoot;
  IlpObject currentEquipmentRoot;
  boolean inDrillDown;
  PropertyChangeSupport propertyChangeSupport;

  /** Logger for local access */
  protected static Logger log = Logger.getLogger(DrillDownManager.class.getName());

  public static final String CURRENT_NETWORK_ROOT = "currentNetworkRoot";
  public static final String CURRENT_EQUIPMENT_ROOT = "currentEquipmentRoot";

  /**
   * Construct an instance that manages the given tree and network components.
   * Both components should share the same datasource.
   */
  public DrillDownManager(IlpTree treeComponent, IlpNetwork networkComponent) {
    this(treeComponent, networkComponent, null);
  }

  /**
   * Construct an instance that manages the given tree and equipment components.
   * Both components should share the same datasource.
   */
  public DrillDownManager(IlpTree treeComponent, IlpEquipment equipmentComponent) {
    this(treeComponent, null, equipmentComponent);
  }

  /**
   * Construct an instance that manages the given tree, network and equipment
   * components. All three components should share the same datasource.
   */
  public DrillDownManager(IlpTree treeComponent, IlpNetwork networkComponent, IlpEquipment equipmentComponent) {
    this.treeComponent = treeComponent;
    this.networkComponent = networkComponent;
    this.equipmentComponent = equipmentComponent;
    this.currentNetworkRoot = null;
    this.currentEquipmentRoot = null;
    this.inDrillDown = false;
    propertyChangeSupport = new PropertyChangeSupport(this);

    TreeSelectionModel treeSelectionModel = treeComponent.getSelectionModel();
    // Only allow single selection in the tree, to simplify computation
    // of corresponding network
    treeSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    // Add the drill down manager as a selection listener for the tree
    // Tree selection is handled specifically in this class, and not delegated
    // to the base class, as selecting an object in the tree triggers a
    // drill-down
    treeSelectionModel.addTreeSelectionListener(this);

    if (networkComponent != null) {
      IlpViewInteractor networkInteractor = networkComponent.getViewInteractor();
      if (networkInteractor != null)
        networkInteractor.setGestureAction(IlpGesture.BUTTON1_DOUBLE_CLICKED, new DrillDownAction(this));
    }
  }

  // --------------------------------------------------------------------------------
  // Basic accessors and other common functions
  // --------------------------------------------------------------------------------
  /**
   * Get the tree component associated with this manager.
   * 
   * @return the tree component. May not be null.
   */
  public IlpTree getTreeComponent() {
    return this.treeComponent;
  }

  /**
   * Get the network component associated with this manager.
   * 
   * @return the network component, or null if there is none
   */
  public IlpNetwork getNetworkComponent() {
    return this.networkComponent;
  }

  /**
   * Get the equipment component associated with this manager.
   * 
   * @return the equipment component, or null if there is none
   */
  public IlpEquipment getEquipmentComponent() {
    return this.equipmentComponent;
  }

  // Action implementation
  public class DrillDownAction extends AbstractAction {
    protected DrillDownManager manager;

    public DrillDownAction(DrillDownManager manager) {
      super();
      this.manager = manager;
    }

    Override
    public void actionPerformed(ActionEvent e) {
      if (e instanceof IlpViewActionEvent) {
        IlpViewActionEvent viewEvent = (IlpViewActionEvent) e;
        IlpObject ilpObj = viewEvent.getIlpObject();
        if (ilpObj != null) {
          log.log(Level.FINE, "Asked to drill down to: " + ilpObj);
          inDrillDown = true;
          showNetworkDetails(ilpObj);
          showEquipmentDetails(ilpObj);
          inDrillDown = false;
          return;
        }
        log.log(Level.FINER, "Double-click action detected at " + viewEvent.getPosition() + ", not on IlpObject");
      } else
        log.log(Level.FINER, "Non-CPL action detected");
    }
  }

  // TreeSelectionListener implementation
  /**
   * Listen for a change in the tree selection. When a new node is selected,
   * change the contents of the network and/or equipment components accordingly.
   */
  Override
  public void valueChanged(TreeSelectionEvent e) {
    if (this.inDrillDown)
      return;

    IlpObjectSelectionModel selectionModel = (IlpObjectSelectionModel) e.getSource();
    IlpObject selectedObject = selectionModel.getSelectedObject();
    showNetworkDetails(selectedObject);
    if (selectedObject != null)
      showEquipmentDetails(selectedObject);
  }

  /**
   * Returns true if the given object is a root object. By default, return
   * objects that have no parents in the datasource.
   */
  public boolean isRootObject(IlpObject obj) {
    IlpChild childItf = treeComponent.getDataSource().getChildInterface(obj);
    return ((childItf == null) || (childItf.getParent(obj) == null));
  }

  // --------------------------------------------------------------------------------
  // Manage drill down for network component
  // --------------------------------------------------------------------------------
  /**
   * Get the parent object of the equipment currently being displayed.
   */
  public IlpObject getCurrentNetworkRoot() {
    return this.currentNetworkRoot;
  }

  /**
   * Set the parent object of the network currently being displayed. This method
   * is protected because it should not be invoked directly.
   */
  protected void setCurrentNetworkRoot(IlpObject root) {
    IlpObject oldRoot = this.currentNetworkRoot;
    this.currentNetworkRoot = root;
    propertyChangeSupport.firePropertyChange(CURRENT_NETWORK_ROOT, oldRoot, root);
  }

  public boolean hasSubNetwork(IlpObject obj) {
    // A more liberal implementation that allows children under non-groups
    // is also possible
    return (obj.getIlpClass().isSubClassOf(IltGroup.GetIlpClass()));
  }

  public void showNetworkDetails(IlpObject obj) {
    log.log(Level.FINE, "Asked to set network root object to " + obj);
    if (networkComponent == null) {
      log.log(Level.FINE, "Network component is null, doing nothing");
      return;
    }

    if (obj == null) {
      showRootNetwork();
    } else if (hasSubNetwork(obj)) {
      // if the object has a sub-network, show it
      showSubNetwork(obj);
    } else {
      // If there is no sub-network, show the
      // network to which this object belongs
      showParentNetwork(obj);
    }
  }

  protected void showRootNetwork() {
    if (getCurrentNetworkRoot() != null) {
      networkComponent.getAdapter().resetOrigins();
      log.log(Level.FINER, "Reset network origins");
      // Remove existing background
      try {
        networkComponent.removeBackgrounds();
      } catch (IlpBackgroundHandlingException ex) {
        log.log(Level.WARNING, "Could not remove bacgrounds with this exception: " + ex.getLocalizedMessage());
      }
      setCurrentNetworkRoot(null);
    } else
      log.log(Level.FINER, "Already showing root network, nothing changed");

  }

  protected void showSubNetwork(IlpObject obj) {
    if (getCurrentNetworkRoot() != obj) {
      List<Object> originList = new ArrayList<Object>();
      originList.add(obj.getIdentifier());
      if (!originList.equals(networkComponent.getAdapter().getOrigins())) {
        // Second parameter is false, as we do not want to show the
        // origins themselves, only their children
        networkComponent.getAdapter().setOrigins(originList, false);
        log.log(Level.FINER, "Set network component origin to " + obj.getIdentifier());

        // Load network configuration
        String templateBaseName = computeSubNetworkTemplateBaseName(obj);
        String configName = templateBaseName + "_config.css";
        IlpURLAccessService urlService = networkComponent.getContext().getURLAccessService();
        try {
          networkComponent.setStyleSheets(urlService.getFileLocation(configName) != null
              ? new String[] { IlpNetwork.DefaultConfigurationFileName, configName }
              : new String[] { IlpNetwork.DefaultConfigurationFileName });
        } catch (Exception e) {
          e.printStackTrace();
        }

        setCurrentNetworkRoot(obj);
      }
    } else
      log.log(Level.FINER, "Already showing sub-network of " + obj.getIdentifier() + ", nothing changed");
  }

  protected void showParentNetwork(IlpObject obj) {
    IlpObject detailObj = null;
    if (isRootObject(obj)) {
      // if the object is a root, show the root network
      showRootNetwork();
      detailObj = obj;
    } else {
      IlpDataSource ds = treeComponent.getDataSource();
      IlpChild childItf = ds.getChildInterface(obj);
      IlpObject parent = ds.getObject(childItf.getParent(obj));

      // Check if the parent has a subnetwork
      // If this object is an equipment detail, its parent will
      // not have a network, so we much check further up the tree
      if (hasSubNetwork(parent)) {
        showSubNetwork(parent);
        detailObj = obj;
      } else {
        // Show the next higher level of parent network
        showParentNetwork(parent);
      }
    }
    if (detailObj != null) {
      // Select the child object, and make sure it is visble
      // networkComponent.getSelectionModel().clearSelection();
      // networkComponent.ensureVisible(detailObj);
      // networkComponent.getSelectionModel().addSelectionObject(detailObj);
      networkComponent.getSelectionModel().setSelectedObjects(Collections.singleton(detailObj));
      networkComponent.ensureVisible(detailObj);
    }
  }

  // --------------------------------------------------------------------------------
  // Manage drill down for equipment component
  // --------------------------------------------------------------------------------
  /**
   * Get the parent object of the equipment currently being displayed.
   */
  public IlpObject getCurrentEquipmentRoot() {
    return this.currentEquipmentRoot;
  }

  /**
   * Set the parent object of the equipment currently being displayed. This
   * method is protected because it should not be invoked directly.
   */
  protected void setCurrentEquipmentRoot(IlpObject root) {
    IlpObject oldRoot = this.currentEquipmentRoot;
    this.currentEquipmentRoot = root;
    propertyChangeSupport.firePropertyChange(CURRENT_EQUIPMENT_ROOT, oldRoot, root);
  }

  public boolean hasEquipmentDetails(IlpObject obj) {
    return ((obj.getIlpClass() == IltNetworkElement.GetIlpClass())
        || obj.getIlpClass().isSubClassOf(IltNetworkElement.GetIlpClass()));
  }

  public boolean isEquipmentDetail(IlpObject obj) {
    return (((obj.getIlpClass() == IltShelf.GetIlpClass()) || obj.getIlpClass().isSubClassOf(IltShelf.GetIlpClass()))
        || obj.getIlpClass().isSubClassOf(IltShelfItem.GetIlpClass())
        || obj.getIlpClass().isSubClassOf(IltCardItem.GetIlpClass()));
  }

  protected void showEquipmentDetails(IlpObject obj) {
    log.log(Level.FINE, "Asked to show equipment details for " + obj);
    if (equipmentComponent == null) {
      log.log(Level.FINE, "Equipment component is null, doing nothing");
      return;
    }

    if (hasEquipmentDetails(obj)) {
      // if equipment details are available, drill down in the equipment
      // component.
      // Second parameter is null, as there is no contained object to select
      setEquipmentRootObject(obj, null);
    } else if (isEquipmentDetail(obj)) {
      // Else if this is an equipment item, show the parent equipment
      // Second obj parameter is needed to have it selected and centered
      showParentEquipment(obj, obj);
    } else // if not equipment related, clear equipment component
      clearEquipmentDetails();
  }

  protected void setEquipmentRootObject(IlpObject parent, IlpObject detailObj) {
    if (getCurrentEquipmentRoot() != parent) {
      setEquipmentOrigin(parent.getIdentifier());
      String templateBaseName = computeEquipmentTemplateBaseName(parent);
      String configName = templateBaseName + "_config.css";
      IlpURLAccessService urlService = equipmentComponent.getContext().getURLAccessService();
      try {
        if (urlService.getFileLocation(configName) != null) {
          log.log(Level.FINER, "Loading config file " + configName);
          equipmentComponent.setStyleSheets(new String[] { IlpEquipment.DefaultConfigurationFileName, configName });
        } else {
          log.log(Level.FINER, "Config file " + configName + " does not exist");
          equipmentComponent.setStyleSheets(new String[] { IlpEquipment.DefaultConfigurationFileName });
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
      setCurrentEquipmentRoot(parent);
    } else
      log.log(Level.FINER, parent.getIdentifier().toString() + " was alreay the root of the equipment view");
    // Select the given contained object, and make sure it is visible
    IlpEquipmentSelectionModel selModel = equipmentComponent.getSelectionModel();
    selModel.setSelectedObject(detailObj);
    if (detailObj != null) {
      equipmentComponent.ensureVisible(detailObj);
    }
  }

  protected void showParentEquipment(IlpObject obj, IlpObject detailObj) {
    // Should only be called for objects with a parent
    IlpDataSource ds = treeComponent.getDataSource();
    IlpChild childItf = ds.getChildInterface(obj);
    IlpObject parent = ds.getObject(childItf.getParent(obj));

    if (hasEquipmentDetails(parent)) {
      // Display the parent
      setEquipmentRootObject(parent, detailObj);
    } else if (isEquipmentDetail(parent)) {
      // recursively try the next higher level
      showParentEquipment(parent, detailObj);
    } else {
      // This should never happen....
      clearEquipmentDetails();
    }
  }

  public void clearEquipmentDetails() {
    if (!equipmentComponent.getAdapter().isShowingOrigin()
        || !Collections.EMPTY_LIST.equals(equipmentComponent.getAdapter().getOrigins())) {
      // Remove existing background
      try {
        equipmentComponent.removeBackgrounds();
      } catch (IlpBackgroundHandlingException ex) {
        log.log(Level.WARNING, "Could not remove bacgrounds with this exception: " + ex.getLocalizedMessage());
      }
      // Clear origins
      equipmentComponent.getAdapter().setOrigins(Collections.emptyList(), true);
      log.log(Level.FINER, "Cleared equipment component origins");
      setCurrentEquipmentRoot(null);
    } else
      log.log(Level.FINER, "Equipment component was already empty, nothing changed");
  }

  void setEquipmentOrigin(Object objId) {
    List<Object> originList = new ArrayList<Object>();
    originList.add(objId);
    if (!originList.equals(equipmentComponent.getAdapter().getOrigins())) {
      // Second parameter is false, as we do not want to show the
      // origins themselves, only their children
      equipmentComponent.getAdapter().setOrigins(originList, false);
      log.log(Level.FINER, "Set equipment component origin to " + objId);
    }
  }

  // --------------------------------------------------------------------------------
  // Manage loading of sub-network and equipment templates
  // --------------------------------------------------------------------------------
  /**
   * Recursively load sub-networks and equipment details for all nodes in the
   * datasource that have them.
   */
  void loadChildren() {
    Collection<IlpObject> rootObjects = treeComponent.getDataSource().getObjects();
    Iterator<IlpObject> iter = rootObjects.iterator();
    while (iter.hasNext())
      loadChildren(iter.next());
  }

  void loadChildren(IlpObject obj) {
    if (hasSubNetwork(obj)) {
      loadTemplate(obj, computeSubNetworkTemplateBaseName(obj) + "_template.xml");
      // Recursively load children
      IlpContainer containerItf = treeComponent.getDataSource().getContainerInterface(obj);
      if (containerItf != null) {
        Collection<Object> children = containerItf.getChildren(obj);
        if ((children != null) && (children.size() > 0)) {
          // Clone list to avoid concurrent modification
          children = new ArrayList<Object>(children);
          Iterator<Object> iter = children.iterator();
          while (iter.hasNext())
            loadChildren(treeComponent.getDataSource().getObject((String) iter.next()));
        }
      }
    }
    if (hasEquipmentDetails(obj)) {
      String templateBaseName = computeEquipmentTemplateBaseName(obj);
      loadTemplate(obj, templateBaseName + "_template.xml");
    }
  }

  /**
   * Compute the name of the subnetwork template to load for a given object
   */
  String computeSubNetworkTemplateBaseName(IlpObject obj) {
    if (obj instanceof IltObject)
      return ((IltObject) obj).getName();
    else {
      String className = obj.getIlpClass().getName();
      return className.substring(className.lastIndexOf('.') + 1);
    }
  }

  /**
   * Compute the name of the equipment template to load for a given object
   */
  String computeEquipmentTemplateBaseName(IlpObject obj) {
    String className = obj.getIlpClass().getName();
    String shortClassName = className.substring(className.lastIndexOf('.') + 1);
    if (obj instanceof IltNetworkElement) {
      IltNetworkElement ne = (IltNetworkElement) obj;
      String typeName = ne.getType().getName();
      return typeName + "_equipment";
    } else
      return shortClassName + "_equipment";
  }

  /**
   * Load a template file into the datasource under a specific parent
   */
  void loadTemplate(IlpObject obj, String templateFileName) {
    log.log(Level.FINE, "Loading template " + templateFileName + " for object " + obj.getIdentifier());
    final Object parentId = obj.getIdentifier();

    // Load the template objects under the parent node, and transform
    // their IDs so they are unique
    try {
      // Create an identifier factory that prepends the parent ID to
      // all identifiers read from the template, to make them unique
      IlpIdentifierFactory idFactory = new IlpIdentifierFactory() {
        Override
        public Object getIdentifier(Object previousIdentifier) {
          return parentId.toString() + "/" + previousIdentifier.toString();
        }
      };
      // Read an XML file into the data source.
      // Configure the XML loader to use the ID factory created above,
      // and to add all objects under the given parent
      IlpDataSourceLoader loader = new IlpDataSourceLoader(templateFileName,
          (IlpMutableDataSource) treeComponent.getDataSource());
      loader.setIdentifierFactory(idFactory);
      loader.setParentIdOfRootObjects(parentId);
      loader.parse();
    } catch (java.lang.Exception e) {
      log.log(Level.WARNING,
          "Failed to find or parse template " + templateFileName + " for object " + obj.getIdentifier());
      System.out.println(e.getMessage());
    }
  }

  // Property change support
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(listener);
  }

  public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
    this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
  }

  public void removePropertyChangeListener(PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(listener);
  }

  public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
    this.propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
  }

}