/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.guitools.controlpanel.ui;

import static org.opends.messages.AdminToolMessages.*;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.TreePath;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.guitools.controlpanel.browser.BasicNodeError;
import org.opends.guitools.controlpanel.browser.BrowserController;
import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent;
import org.opends.guitools.controlpanel.event.EntryReadErrorEvent;
import org.opends.guitools.controlpanel.event.EntryReadEvent;
import org.opends.guitools.controlpanel.event.EntryReadListener;
import org.opends.guitools.controlpanel.event.LDAPEntryChangedEvent;
import org.opends.guitools.controlpanel.event.LDAPEntryChangedListener;
import org.opends.guitools.controlpanel.task.DeleteEntryTask;
import org.opends.guitools.controlpanel.task.ModifyEntryTask;
import org.opends.guitools.controlpanel.task.Task;
import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.server.config.ConfigConstants;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.OpenDsException;
import org.opends.server.util.ServerConstants;

/** This is the panel that contains all the different views to display an entry. */
public class LDAPEntryPanel extends StatusGenericPanel
implements EntryReadListener
{
  private static final long serialVersionUID = -6608246173472437830L;
  private JButton saveChanges;
  private JButton delete;
  private JPanel mainPanel;
  private CardLayout cardLayout;

  private ErrorSearchingEntryPanel errorSearchingPanel;
  private LDIFViewEntryPanel ldifEntryPanel;
  private TableViewEntryPanel tableEntryPanel;
  private SimplifiedViewEntryPanel simplifiedEntryPanel;

  private ViewEntryPanel displayedEntryPanel;

  private CustomSearchResult searchResult;
  private BrowserController controller;
  private TreePath treePath;

  private ModifyEntryTask newTask;

  private final String NOTHING_SELECTED = "Nothing Selected";
  private final String MULTIPLE_SELECTED = "Multiple Selected";
  private final String LDIF_VIEW = "LDIF View";
  private final String ATTRIBUTE_VIEW = "Attribute View";
  private final String SIMPLIFIED_VIEW = "Simplified View";
  private final String ERROR_SEARCHING = "Error Searching";

  private View view = View.SIMPLIFIED_VIEW;

  /** The different views that we have to display an LDAP entry. */
  public enum View
  {
    /** Simplified view. */
    SIMPLIFIED_VIEW,
    /** Attribute view (contained in a table). */
    ATTRIBUTE_VIEW,
    /** LDIF view (text based). */
    LDIF_VIEW
  }

  /** Default constructor. */
  public LDAPEntryPanel()
  {
    super();
    createLayout();
  }

  /** Creates the layout of the panel (but the contents are not populated here). */
  private void createLayout()
  {
    GridBagConstraints gbc = new GridBagConstraints();
    cardLayout = new CardLayout();
    mainPanel = new JPanel(cardLayout);
    mainPanel.setOpaque(false);
    gbc.gridx = 0;
    gbc.gridy = 0;
    gbc.gridwidth = 2;
    gbc.weightx = 1.0;
    gbc.weighty = 1.0;
    gbc.fill = GridBagConstraints.BOTH;
    add(mainPanel, gbc);
    gbc.gridwidth = 1;
    gbc.anchor = GridBagConstraints.WEST;
    gbc.insets = new Insets(5, 5, 5, 5);
    gbc.weighty = 0.0;
    gbc.gridy ++;
    gbc.fill = GridBagConstraints.NONE;
    delete = Utilities.createButton(INFO_CTRL_PANEL_DELETE_ENTRY_BUTTON.get());
    delete.setOpaque(false);
    add(delete, gbc);
    delete.addActionListener(new ActionListener()
    {
      public void actionPerformed(ActionEvent ev)
      {
        deleteEntry();
      }
    });

    gbc.anchor = GridBagConstraints.EAST;
    gbc.gridx ++;
    saveChanges =
      Utilities.createButton(INFO_CTRL_PANEL_SAVE_CHANGES_LABEL.get());
    saveChanges.setOpaque(false);
    add(saveChanges, gbc);
    saveChanges.addActionListener(new ActionListener()
    {
      /** {@inheritDoc} */
      public void actionPerformed(ActionEvent ev)
      {
        saveChanges(true);
      }
    });

    Border border = new EmptyBorder(10, 10, 10, 10);

    NoItemSelectedPanel noEntryPanel = new NoItemSelectedPanel();
    noEntryPanel.setMessage(INFO_CTRL_PANEL_NO_ENTRY_SELECTED_LABEL.get());
    Utilities.setBorder(noEntryPanel, border);
    mainPanel.add(noEntryPanel, NOTHING_SELECTED);

    NoItemSelectedPanel multipleEntryPanel = new NoItemSelectedPanel();
    multipleEntryPanel.setMessage(
        INFO_CTRL_PANEL_MULTIPLE_ENTRIES_SELECTED_LABEL.get());
    Utilities.setBorder(multipleEntryPanel, border);
    mainPanel.add(multipleEntryPanel, MULTIPLE_SELECTED);

    errorSearchingPanel = new ErrorSearchingEntryPanel();
    if (errorSearchingPanel.requiresBorder())
    {
      Utilities.setBorder(multipleEntryPanel, border);
    }
    mainPanel.add(errorSearchingPanel, ERROR_SEARCHING);

    LDAPEntryChangedListener listener = new LDAPEntryChangedListener()
    {
      /** {@inheritDoc} */
      public void entryChanged(LDAPEntryChangedEvent ev)
      {
        boolean enable = saveChanges.isVisible() &&
            !authenticationRequired(getInfo().getServerDescriptor());
        if (enable)
        {
          if (ev.getEntry() == null)
          {
            // Something changed that is wrong: assume the entry has been
            // modified, when the user tries to save we will inform of the
            // problem
            enable = true;
          }
          else
          {
            boolean modified =
              !Utilities.areDnsEqual(ev.getEntry().getName().toString(),
                  searchResult.getDN()) ||
                  !ModifyEntryTask.getModifications(ev.getEntry(), searchResult,
                      getInfo()).isEmpty();
            enable = modified;
          }
        }
        saveChanges.setEnabled(enable);
      }
    };

    ldifEntryPanel = new LDIFViewEntryPanel();
    ldifEntryPanel.addLDAPEntryChangedListener(listener);
    if (ldifEntryPanel.requiresBorder())
    {
      Utilities.setBorder(ldifEntryPanel, border);
    }
    mainPanel.add(ldifEntryPanel, LDIF_VIEW);

    tableEntryPanel = new TableViewEntryPanel();
    tableEntryPanel.addLDAPEntryChangedListener(listener);
    if (tableEntryPanel.requiresBorder())
    {
      Utilities.setBorder(tableEntryPanel, border);
    }
    mainPanel.add(tableEntryPanel, ATTRIBUTE_VIEW);

    simplifiedEntryPanel = new SimplifiedViewEntryPanel();
    simplifiedEntryPanel.addLDAPEntryChangedListener(listener);
    if (simplifiedEntryPanel.requiresBorder())
    {
      Utilities.setBorder(simplifiedEntryPanel, border);
    }
    mainPanel.add(simplifiedEntryPanel, SIMPLIFIED_VIEW);

    cardLayout.show(mainPanel, NOTHING_SELECTED);
  }

  /** {@inheritDoc} */
  public void okClicked()
  {
    // No ok button
  }

  /** {@inheritDoc} */
  public void entryRead(EntryReadEvent ev)
  {
    searchResult = ev.getSearchResult();

    updateEntryView(searchResult, treePath);
  }

  /**
   * Updates the panel with the provided search result.
   * @param searchResult the search result corresponding to the selected node.
   * @param treePath the tree path of the selected node.
   */
  private void updateEntryView(CustomSearchResult searchResult,
      TreePath treePath)
  {
    boolean isReadOnly = isReadOnly(searchResult.getDN());
    boolean canDelete = canDelete(searchResult.getDN());

    delete.setVisible(canDelete);
    saveChanges.setVisible(!isReadOnly);
    String cardKey;
    switch (view)
    {
    case LDIF_VIEW:
      displayedEntryPanel = ldifEntryPanel;
      cardKey = LDIF_VIEW;
      break;
    case ATTRIBUTE_VIEW:
      displayedEntryPanel = tableEntryPanel;
      cardKey = ATTRIBUTE_VIEW;
      break;
    default:
      displayedEntryPanel = simplifiedEntryPanel;
      cardKey = SIMPLIFIED_VIEW;
    }
    displayedEntryPanel.update(searchResult, isReadOnly, treePath);
    saveChanges.setEnabled(false);
    cardLayout.show(mainPanel, cardKey);
  }

  /**
   * Sets the view to be displayed by this panel.
   * @param view the view.
   */
  public void setView(View view)
  {
    if (view != this.view)
    {
      this.view = view;
      if (searchResult != null)
      {
        updateEntryView(searchResult, treePath);
      }
    }
  }

  /**
   * Displays a message informing that an error occurred reading the entry.
   * @param ev the entry read error event.
   */
  public void entryReadError(EntryReadErrorEvent ev)
  {
    searchResult = null;

    errorSearchingPanel.setError(ev.getDN(), ev.getError());

    delete.setVisible(false);
    saveChanges.setVisible(false);

    cardLayout.show(mainPanel, ERROR_SEARCHING);

    displayedEntryPanel = null;
  }

  /**
   * Displays a message informing that an error occurred resolving a referral.
   * @param dn the DN of the local entry.
   * @param referrals the list of referrals defined in the entry.
   * @param error the error that occurred resolving the referral.
   */
  public void referralSolveError(String dn, String[] referrals,
      BasicNodeError error)
  {
    searchResult = null;

    errorSearchingPanel.setReferralError(dn, referrals, error);

    delete.setVisible(false);
    saveChanges.setVisible(false);

    cardLayout.show(mainPanel, ERROR_SEARCHING);

    displayedEntryPanel = null;
  }

  /** Displays a panel informing that nothing is selected. */
  public void noEntrySelected()
  {
    searchResult = null;

    delete.setVisible(false);
    saveChanges.setVisible(false);

    cardLayout.show(mainPanel, NOTHING_SELECTED);

    displayedEntryPanel = null;
  }

  /** Displays a panel informing that multiple entries are selected. */
  public void multipleEntriesSelected()
  {
    searchResult = null;

    delete.setVisible(false);
    saveChanges.setVisible(false);

    cardLayout.show(mainPanel, MULTIPLE_SELECTED);

    displayedEntryPanel = null;
  }

  /** {@inheritDoc} */
  public GenericDialog.ButtonType getButtonType()
  {
    return GenericDialog.ButtonType.NO_BUTTON;
  }

  /** {@inheritDoc} */
  public LocalizableMessage getTitle()
  {
    return INFO_CTRL_PANEL_EDIT_LDAP_ENTRY_TITLE.get();
  }

  /** {@inheritDoc} */
  public Component getPreferredFocusComponent()
  {
    return saveChanges;
  }

  /** {@inheritDoc} */
  public void configurationChanged(ConfigurationChangeEvent ev)
  {
    final ServerDescriptor desc = ev.getNewDescriptor();
    SwingUtilities.invokeLater(new Runnable()
    {
      /** {@inheritDoc} */
      public void run()
      {
        boolean isReadOnly = true;
        boolean canDelete = false;
        if (searchResult != null && desc.isAuthenticated())
        {
          isReadOnly = isReadOnly(searchResult.getDN());
          canDelete = canDelete(searchResult.getDN());
        }

        delete.setVisible(canDelete);
        saveChanges.setVisible(!isReadOnly);
      }
    });
  }

  /** {@inheritDoc} */
  public void setInfo(ControlPanelInfo info)
  {
    super.setInfo(info);
    simplifiedEntryPanel.setInfo(info);
    ldifEntryPanel.setInfo(info);
    tableEntryPanel.setInfo(info);
    errorSearchingPanel.setInfo(info);
  }

  private List<DN> parentReadOnly;
  private List<DN> nonDeletable;
  {
    try
    {
      parentReadOnly = Arrays.asList(
        DN.valueOf(ConfigConstants.DN_TASK_ROOT),
        DN.valueOf(ConfigConstants.DN_MONITOR_ROOT),
        DN.valueOf(ConfigConstants.DN_BACKUP_ROOT),
        DN.valueOf(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT)
      );
      nonDeletable = Arrays.asList(
          DN.valueOf(ConfigConstants.DN_CONFIG_ROOT),
          DN.valueOf(ConfigConstants.DN_DEFAULT_SCHEMA_ROOT),
          DN.valueOf(ConfigConstants.DN_TRUST_STORE_ROOT)
      );
    }
    catch (Throwable t)
    {
      throw new RuntimeException("Error decoding DNs: "+t, t);
    }
  }

  /**
   * Returns <CODE>true</CODE> if the provided DN corresponds to a read-only
   * entry and <CODE>false</CODE> otherwise.
   * @param sDn the DN of the entry.
   * @return <CODE>true</CODE> if the provided DN corresponds to a read-only
   * entry and <CODE>false</CODE> otherwise.
   */
  public boolean isReadOnly(String sDn)
  {
    boolean isReadOnly = false;
    try
    {
      DN dn = DN.valueOf(sDn);
      for (DN parentDN : parentReadOnly)
      {
        if (dn.isDescendantOf(parentDN))
        {
          isReadOnly = true;
          break;
        }
      }
      if (!isReadOnly)
      {
        isReadOnly = dn.equals(DN.NULL_DN);
      }
    }
    catch (Throwable t)
    {
      throw new RuntimeException("Error decoding DNs: "+t, t);
    }
    return isReadOnly;
  }

  /**
   * Returns <CODE>true</CODE> if the provided DN corresponds to an entry that
   * can be deleted and <CODE>false</CODE> otherwise.
   * @param sDn the DN of the entry.
   * @return <CODE>true</CODE> if the provided DN corresponds to an entry that
   * can be deleted and <CODE>false</CODE> otherwise.
   */
  public boolean canDelete(String sDn)
  {
    try
    {
      DN dn = DN.valueOf(sDn);
      return !dn.equals(DN.NULL_DN)
          && !nonDeletable.contains(dn)
          && isDescendantOfAny(dn, parentReadOnly);
    }
    catch (Throwable t)
    {
      throw new RuntimeException("Error decoding DNs: "+t, t);
    }
  }

  private boolean isDescendantOfAny(DN dn, List<DN> parentDNs)
  {
    for (DN parentDN : parentDNs)
    {
      if (dn.isDescendantOf(parentDN))
      {
        return false;
      }
    }
    return true;
  }

  /**
   * Saves the changes done to the entry.
   * @param modal whether the progress dialog for the task must be modal or
   * not.
   */
  private void saveChanges(boolean modal)
  {
    newTask = null;
    final ArrayList<LocalizableMessage> errors = new ArrayList<>();
    // Check that the entry is correct.
    try
    {
      ProgressDialog dlg = new ProgressDialog(
          Utilities.getFrame(this),
          Utilities.getFrame(this),
          INFO_CTRL_PANEL_MODIFYING_ENTRY_CHANGES_TITLE.get(), getInfo());
      dlg.setModal(modal);
      Entry entry = displayedEntryPanel.getEntry();
      newTask = new ModifyEntryTask(getInfo(), dlg, entry,
            searchResult, controller, treePath);
      for (Task task : getInfo().getTasks())
      {
        task.canLaunch(newTask, errors);
      }

      if (errors.isEmpty())
      {
        if (newTask.hasModifications()) {
          String dn = entry.getName().toString();
          launchOperation(newTask,
              INFO_CTRL_PANEL_MODIFYING_ENTRY_SUMMARY.get(dn),
              INFO_CTRL_PANEL_MODIFYING_ENTRY_COMPLETE.get(),
              INFO_CTRL_PANEL_MODIFYING_ENTRY_SUCCESSFUL.get(dn),
              ERR_CTRL_PANEL_MODIFYING_ENTRY_ERROR_SUMMARY.get(),
              ERR_CTRL_PANEL_MODIFYING_ENTRY_ERROR_DETAILS.get(dn),
              null,
              dlg);
          saveChanges.setEnabled(false);
          dlg.setVisible(true);
        }
        else
        {
          // Mark the panel as it has no changes.  This can happen because every
          // time the user types something the saveChanges button is enabled
          // (for performance reasons with huge entries).
          saveChanges.setEnabled(false);
        }
      }
    }
    catch (OpenDsException ode)
    {
      errors.add(ERR_CTRL_PANEL_INVALID_ENTRY.get(ode.getMessageObject()));
    }
    if (!errors.isEmpty())
    {
      displayErrorDialog(errors);
    }
  }

  private void deleteEntry()
  {
    final ArrayList<LocalizableMessage> errors = new ArrayList<>();
    // Check that the entry is correct.
    // Rely in numsubordinates and hassubordinates
    boolean isLeaf = !BrowserController.getHasSubOrdinates(searchResult);

    if (treePath != null)
    {
      LocalizableMessage title = isLeaf ? INFO_CTRL_PANEL_DELETING_ENTRY_TITLE.get() :
        INFO_CTRL_PANEL_DELETING_SUBTREE_TITLE.get();
      ProgressDialog dlg = new ProgressDialog(
          Utilities.createFrame(),
          Utilities.getParentDialog(this), title, getInfo());
      DeleteEntryTask newTask = new DeleteEntryTask(getInfo(), dlg,
          new TreePath[]{treePath}, controller);
      for (Task task : getInfo().getTasks())
      {
        task.canLaunch(newTask, errors);
      }
      if (errors.isEmpty())
      {
        LocalizableMessage confirmationMessage =
          isLeaf ? INFO_CTRL_PANEL_DELETE_ENTRY_CONFIRMATION_DETAILS.get(
              searchResult.getDN()) :
                INFO_CTRL_PANEL_DELETE_SUBTREE_CONFIRMATION_DETAILS.get(
                    searchResult.getDN());
          if (displayConfirmationDialog(
              INFO_CTRL_PANEL_CONFIRMATION_REQUIRED_SUMMARY.get(),
              confirmationMessage))
          {
            String dn = searchResult.getDN();
            if (isLeaf)
            {
              launchOperation(newTask,
                  INFO_CTRL_PANEL_DELETING_ENTRY_SUMMARY.get(dn),
                  INFO_CTRL_PANEL_DELETING_ENTRY_COMPLETE.get(),
                  INFO_CTRL_PANEL_DELETING_ENTRY_SUCCESSFUL.get(dn),
                  ERR_CTRL_PANEL_DELETING_ENTRY_ERROR_SUMMARY.get(),
                  ERR_CTRL_PANEL_DELETING_ENTRY_ERROR_DETAILS.get(dn),
                  null,
                  dlg);
            }
            else
            {
              launchOperation(newTask,
                  INFO_CTRL_PANEL_DELETING_SUBTREE_SUMMARY.get(dn),
                  INFO_CTRL_PANEL_DELETING_SUBTREE_COMPLETE.get(),
                  INFO_CTRL_PANEL_DELETING_SUBTREE_SUCCESSFUL.get(dn),
                  ERR_CTRL_PANEL_DELETING_SUBTREE_ERROR_SUMMARY.get(),
                  ERR_CTRL_PANEL_DELETING_SUBTREE_ERROR_DETAILS.get(dn),
                  null,
                  dlg);
            }
            dlg.setVisible(true);
          }
      }
    }
    if (!errors.isEmpty())
    {
      displayErrorDialog(errors);
    }
  }

  /**
   * Returns the browser controller in charge of the tree.
   * @return the browser controller in charge of the tree.
   */
  public BrowserController getController()
  {
    return controller;
  }

  /**
   * Sets the browser controller in charge of the tree.
   * @param controller the browser controller in charge of the tree.
   */
  public void setController(BrowserController controller)
  {
    this.controller = controller;
  }

  /**
   * Returns the tree path associated with the node that is being displayed.
   * @return the tree path associated with the node that is being displayed.
   */
  public TreePath getTreePath()
  {
    return treePath;
  }

  /**
   * Sets the tree path associated with the node that is being displayed.
   * @param treePath the tree path associated with the node that is being
   * displayed.
   */
  public void setTreePath(TreePath treePath)
  {
    this.treePath = treePath;
  }

  /**
   * Method used to know if there are unsaved changes or not.  It is used by
   * the entry selection listener when the user changes the selection.
   * @return <CODE>true</CODE> if there are unsaved changes (and so the
   * selection of the entry should be cancelled) and <CODE>false</CODE>
   * otherwise.
   */
  public boolean mustCheckUnsavedChanges()
  {
    return displayedEntryPanel != null &&
        saveChanges.isVisible() && saveChanges.isEnabled();
  }

  /**
   * Tells whether the user chose to save the changes in the panel, to not save
   * them or simply canceled the selection change in the tree.
   * @return the value telling whether the user chose to save the changes in the
   * panel, to not save them or simply canceled the selection in the tree.
   */
  public UnsavedChangesDialog.Result checkUnsavedChanges()
  {
    UnsavedChangesDialog.Result result;
    UnsavedChangesDialog unsavedChangesDlg = new UnsavedChangesDialog(
          Utilities.getParentDialog(this), getInfo());
    unsavedChangesDlg.setMessage(INFO_CTRL_PANEL_UNSAVED_CHANGES_SUMMARY.get(),
       INFO_CTRL_PANEL_UNSAVED_ENTRY_CHANGES_DETAILS.get(searchResult.getDN()));
    Utilities.centerGoldenMean(unsavedChangesDlg,
          Utilities.getParentDialog(this));
    unsavedChangesDlg.setVisible(true);
    result = unsavedChangesDlg.getResult();
    if (result == UnsavedChangesDialog.Result.SAVE)
    {
      saveChanges(false);
      if (newTask == null || // The user data is not valid
          newTask.getState() != Task.State.FINISHED_SUCCESSFULLY)
      {
        result = UnsavedChangesDialog.Result.CANCEL;
      }
    }

    return result;
  }
}
