/*
 * 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 2011-2015 ForgeRock AS
 */

package org.opends.guitools.controlpanel.ui;

import static org.opends.messages.AdminToolMessages.*;
import static org.opends.messages.ToolMessages.*;
import static org.opends.server.util.ServerConstants.*;

import static com.forgerock.opendj.util.OperatingSystem.*;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumn;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.guitools.controlpanel.datamodel.BackupDescriptor;
import org.opends.guitools.controlpanel.datamodel.BackupTableModel;
import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
import org.opends.guitools.controlpanel.event.BrowseActionListener;
import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent;
import org.opends.guitools.controlpanel.ui.renderer.BackupTableCellRenderer;
import org.opends.guitools.controlpanel.util.BackgroundTask;
import org.opends.guitools.controlpanel.util.Utilities;
import org.opends.quicksetup.Installation;
import org.opends.server.types.BackupDirectory;
import org.opends.server.types.BackupInfo;
import org.opends.server.util.StaticUtils;

/** Abstract class used to refactor code in panels that contain a backup list on it. */
public abstract class BackupListPanel extends StatusGenericPanel
{
  private static final long serialVersionUID = -4804555239922795163L;

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The refreshing list message, displayed when the list of backups is refreshed. */
  protected static final LocalizableMessage REFRESHING_LIST = INFO_CTRL_PANEL_REFRESHING_LIST_SUMMARY.get();

  /** The message informing that no backups where found. */
  protected static final LocalizableMessage NO_BACKUPS_FOUND = INFO_CTRL_PANEL_NO_BACKUPS_FOUND.get();

  private static final String DUMMY_PARENT_PATH = "/local/OpenDJ-X.X.X/bak";

  /** The text field containing the parent directory. */
  protected JTextField parentDirectory;

  /** Label for the path field. */
  protected JLabel lPath;

  /** Label for the list. */
  protected JLabel lAvailableBackups;

  /** Refreshing list label (displayed instead of the list when this one is being refreshed). */
  protected JLabel lRefreshingList;

  /** Refresh list button. */
  protected JButton refreshList;

  /** Verify backup button. */
  protected JButton verifyBackup;

  /** Browse button. */
  protected JButton browse;

  /** The scroll that contains the list of backups (actually is a table). */
  protected JScrollPane tableScroll;

  /** The list of backups. */
  protected JTable backupList;

  private JLabel lRemoteFileHelp;

  /** Whether the backup parent directory has been initialized with a value. */
  private boolean backupDirectoryInitialized;

  private BackupTableCellRenderer renderer;

  /** Default constructor. */
  protected BackupListPanel()
  {
    super();
  }

  @Override
  public Component getPreferredFocusComponent()
  {
    return parentDirectory;
  }

  /**
   * Returns the selected backup in the list.
   *
   * @return the selected backup in the list.
   */
  protected BackupDescriptor getSelectedBackup()
  {
    BackupDescriptor backup = null;
    int row = backupList.getSelectedRow();
    if (row != -1)
    {
      BackupTableModel model = (BackupTableModel) backupList.getModel();
      backup = model.get(row);
    }
    return backup;
  }

  /**
   * Notification that the verify button was clicked. Whatever is required to be
   * done must be done in this method.
   */
  protected abstract void verifyBackupClicked();

  /**
   * Creates the components and lays them in the panel.
   *
   * @param gbc
   *          the grid bag constraints to be used.
   */
  protected void createLayout(GridBagConstraints gbc)
  {
    gbc.gridy++;
    gbc.anchor = GridBagConstraints.WEST;
    gbc.weightx = 0.0;
    gbc.fill = GridBagConstraints.NONE;
    gbc.gridwidth = 1;
    gbc.insets.left = 0;
    lPath = Utilities.createPrimaryLabel(INFO_CTRL_PANEL_BACKUP_PATH_LABEL.get());
    add(lPath, gbc);

    gbc.gridx = 1;
    gbc.insets.left = 10;
    parentDirectory = Utilities.createLongTextField();
    gbc.weightx = 1.0;
    gbc.fill = GridBagConstraints.HORIZONTAL;
    add(parentDirectory, gbc);
    browse = Utilities.createButton(INFO_CTRL_PANEL_BROWSE_BUTTON_LABEL.get());
    browse.setOpaque(false);
    browse.addActionListener(
        new BrowseActionListener(parentDirectory, BrowseActionListener.BrowseType.LOCATION_DIRECTORY, this));
    gbc.gridx = 2;
    gbc.weightx = 0.0;
    add(browse, gbc);

    lRemoteFileHelp = Utilities.createInlineHelpLabel(INFO_CTRL_PANEL_REMOTE_SERVER_PATH.get());
    gbc.gridx = 1;
    gbc.gridwidth = 2;
    gbc.insets.top = 3;
    gbc.insets.left = 10;
    gbc.gridy++;
    add(lRemoteFileHelp, gbc);

    gbc.gridx = 0;
    gbc.gridy++;
    gbc.insets.top = 10;
    gbc.insets.left = 0;
    lAvailableBackups = Utilities.createPrimaryLabel(INFO_CTRL_PANEL_AVAILABLE_BACKUPS_LABEL.get());
    gbc.anchor = GridBagConstraints.NORTHWEST;
    gbc.fill = GridBagConstraints.NONE;
    gbc.gridwidth = 1;
    add(lAvailableBackups, gbc);

    gbc.gridx = 1;
    gbc.gridwidth = 2;
    gbc.weightx = 1.0;
    gbc.weighty = 1.0;
    gbc.fill = GridBagConstraints.BOTH;
    gbc.insets.left = 10;
    lRefreshingList = Utilities.createDefaultLabel(REFRESHING_LIST);
    lRefreshingList.setHorizontalAlignment(SwingConstants.CENTER);
    gbc.anchor = GridBagConstraints.CENTER;
    add(lRefreshingList, gbc);

    backupList = new JTable();
    // Done to provide a good size to the table.
    BackupTableModel model = new BackupTableModel();
    for (BackupDescriptor backup : createDummyBackupList())
    {
      model.add(backup);
    }
    backupList.setModel(model);
    backupList.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    backupList.setShowGrid(false);
    backupList.setIntercellSpacing(new Dimension(0, 0));
    renderer = new BackupTableCellRenderer();
    renderer.setParentPath(new File(DUMMY_PARENT_PATH));
    for (int i = 0; i < model.getColumnCount(); i++)
    {
      TableColumn col = backupList.getColumn(model.getColumnName(i));
      col.setCellRenderer(renderer);
    }
    backupList.setTableHeader(null);
    Utilities.updateTableSizes(backupList);
    tableScroll = Utilities.createScrollPane(backupList);
    tableScroll.setColumnHeaderView(null);
    tableScroll.setPreferredSize(backupList.getPreferredSize());
    gbc.anchor = GridBagConstraints.NORTHWEST;
    add(tableScroll, gbc);
    lRefreshingList.setPreferredSize(tableScroll.getPreferredSize());

    gbc.gridy++;
    gbc.anchor = GridBagConstraints.EAST;
    gbc.weightx = 0.0;
    gbc.weighty = 0.0;
    gbc.insets.top = 5;
    JPanel buttonPanel = new JPanel(new GridBagLayout());
    buttonPanel.setOpaque(false);
    add(buttonPanel, gbc);
    GridBagConstraints gbc2 = new GridBagConstraints();
    gbc2.gridx = 0;
    gbc2.gridy = 0;
    gbc2.gridwidth = 1;
    gbc2.anchor = GridBagConstraints.EAST;
    gbc2.fill = GridBagConstraints.HORIZONTAL;
    gbc2.weightx = 1.0;
    buttonPanel.add(Box.createHorizontalGlue(), gbc2);
    refreshList = Utilities.createButton(INFO_CTRL_PANEL_REFRESH_LIST_BUTTON_LABEL.get());
    refreshList.setOpaque(false);
    refreshList.addActionListener(new ActionListener()
    {
      @Override
      public void actionPerformed(ActionEvent ev)
      {
        refreshList();
      }
    });
    gbc2.weightx = 0.0;
    gbc2.gridx++;
    buttonPanel.add(refreshList, gbc2);
    gbc2.gridx++;
    gbc2.insets.left = 5;
    verifyBackup = Utilities.createButton(INFO_CTRL_PANEL_VERIFY_BACKUP_BUTTON_LABEL.get());
    verifyBackup.setOpaque(false);
    verifyBackup.addActionListener(new ActionListener()
    {
      @Override
      public void actionPerformed(ActionEvent ev)
      {
        verifyBackupClicked();
      }
    });
    ListSelectionListener listener = new ListSelectionListener()
    {
      @Override
      public void valueChanged(ListSelectionEvent ev)
      {
        BackupDescriptor backup = getSelectedBackup();
        verifyBackup.setEnabled(backup != null);
      }
    };
    backupList.getSelectionModel().addListSelectionListener(listener);
    listener.valueChanged(null);
    buttonPanel.add(verifyBackup, gbc2);
  }

  /**
   * Refresh the list of backups by looking in the backups defined under the
   * provided parent backup directory.
   */
  protected void refreshList()
  {
    final boolean refreshEnabled = refreshList.isEnabled();
    refreshList.setEnabled(false);
    verifyBackup.setEnabled(false);
    tableScroll.setVisible(false);
    lRefreshingList.setText(REFRESHING_LIST.toString());
    lRefreshingList.setVisible(isLocal());

    final int lastSelectedRow = backupList.getSelectedRow();
    final String parentPath = parentDirectory.getText();

    BackgroundTask<Set<BackupInfo>> worker = new BackgroundTask<Set<BackupInfo>>()
    {
      @Override
      public Set<BackupInfo> processBackgroundTask() throws Throwable
      {
        // Open the backup directory and make sure it is valid.
        Set<BackupInfo> backups = new LinkedHashSet<>();
        Throwable firstThrowable = null;

        if (new File(parentPath, BACKUP_DIRECTORY_DESCRIPTOR_FILE).exists())
        {
          try
          {
            BackupDirectory backupDir = BackupDirectory.readBackupDirectoryDescriptor(parentPath);
            backups.addAll(backupDir.getBackups().values());
          }
          catch (Throwable t)
          {
            firstThrowable = t;
          }
        }

        // Check the subdirectories
        File f = new File(parentPath);

        // Check the first level of directories (we might have done
        // a backup of one backend and then a backup of several backends under the same directory).
        if (f.isDirectory())
        {
          File[] children = f.listFiles();
          for (int i = 0; i < children.length; i++)
          {
            if (children[i].isDirectory())
            {
              try
              {
                BackupDirectory backupDir =
                    BackupDirectory.readBackupDirectoryDescriptor(children[i].getAbsolutePath());

                backups.addAll(backupDir.getBackups().values());
              }
              catch (Throwable t2)
              {
                if (!children[i].getName().equals("tasks") && firstThrowable != null)
                {
                  logger.warn(LocalizableMessage.raw("Error searching backup: " + t2, t2));
                }
              }
            }
          }
        }
        if (backups.isEmpty() && firstThrowable != null)
        {
          throw firstThrowable;
        }
        return backups;
      }

      @Override
      public void backgroundTaskCompleted(Set<BackupInfo> returnValue, Throwable t)
      {
        BackupTableModel model = (BackupTableModel) backupList.getModel();
        model.clear();
        renderer.setParentPath(new File(parentPath));
        if (t == null)
        {
          performSuccessActions(returnValue, model);
        }
        else
        {
          performErrorActions(t, model);
        }

        refreshList.setEnabled(refreshEnabled);
        verifyBackup.setEnabled(getSelectedBackup() != null);
        if (lastSelectedRow != -1 && lastSelectedRow < backupList.getRowCount())
        {
          backupList.setRowSelectionInterval(lastSelectedRow, lastSelectedRow);
        }
        else if (backupList.getRowCount() > 0)
        {
          backupList.setRowSelectionInterval(0, 0);
        }
      }

      private void performSuccessActions(Set<BackupInfo> returnValue, BackupTableModel model)
      {
        if (!returnValue.isEmpty())
        {
          for (BackupInfo backup : returnValue)
          {
            model.add(new BackupDescriptor(backup));
          }
          Utilities.updateTableSizes(backupList);
          tableScroll.setVisible(true);
          lRefreshingList.setVisible(false);
        }
        else
        {
          lRefreshingList.setText(NO_BACKUPS_FOUND.toString());
          lRefreshingList.setVisible(isLocal());
        }
        updateUI(true, model);
      }

      private void performErrorActions(Throwable t, BackupTableModel model)
      {
        LocalizableMessage details = ERR_RESTOREDB_CANNOT_READ_BACKUP_DIRECTORY.get(
            parentDirectory.getText(), StaticUtils.getExceptionMessage(t));
        updateErrorPane(errorPane,
                        ERR_ERROR_SEARCHING_BACKUPS_SUMMARY.get(),
                        ColorAndFontConstants.errorTitleFont,
                        details,
                        errorPane.getFont());
        packParentDialog();
        updateUI(false, model);
     }

      private void updateUI(boolean isSuccess, BackupTableModel model)
      {
        model.fireTableDataChanged();
        errorPane.setVisible(!isSuccess);
        if (isSuccess)
        {
          // This is done to perform checks against whether we require to display an error message.
          configurationChanged(new ConfigurationChangeEvent(null, getInfo().getServerDescriptor()));
        }
        else
        {
          lRefreshingList.setText(NO_BACKUPS_FOUND.toString());
        }
      }
    };
    worker.startBackgroundTask();
  }


  /**
   * Creates a list with backup descriptor.
   * This is done simply to have a good initial size for the table.
   *
   * @return a list with bogus backup descriptors.
   */
  private List<BackupDescriptor> createDummyBackupList()
  {
    List<BackupDescriptor> list = new ArrayList<>();
    list.add(new BackupDescriptor(new File(DUMMY_PARENT_PATH + "/200704201567Z"),
             new GregorianCalendar(2007, 5, 20, 8, 10).getTime(), BackupDescriptor.Type.FULL, "id"));
    list.add(new BackupDescriptor(new File(DUMMY_PARENT_PATH + "/200704201567Z"),
             new GregorianCalendar(2007, 5, 22, 8, 10).getTime(), BackupDescriptor.Type.INCREMENTAL, "id"));
    list.add(new BackupDescriptor(new File(DUMMY_PARENT_PATH + "/200704221567Z"),
             new GregorianCalendar(2007, 5, 25, 8, 10).getTime(), BackupDescriptor.Type.INCREMENTAL, "id"));
    return list;
  }

  @Override
  public void configurationChanged(final ConfigurationChangeEvent ev)
  {
    if (!backupDirectoryInitialized && parentDirectory.getText().length() == 0)
    {
      SwingUtilities.invokeLater(new Runnable()
      {
        @Override
        public void run()
        {
          parentDirectory.setText(getBackupPath(ev.getNewDescriptor()));
          refreshList();
          backupDirectoryInitialized = true;
        }
      });
    }

    SwingUtilities.invokeLater(new Runnable()
    {
      @Override
      public void run()
      {
        lRemoteFileHelp.setVisible(!isLocal());
        browse.setVisible(isLocal());
        lAvailableBackups.setVisible(isLocal());
        tableScroll.setVisible(isLocal());
        refreshList.setVisible(isLocal());
        verifyBackup.setVisible(isLocal());
      }
    });
  }

  private String getBackupPath(ServerDescriptor desc)
  {
    if (desc.isLocal() || desc.isWindows() == isWindows())
    {
      File f = new File(desc.getInstancePath(), Installation.BACKUPS_PATH_RELATIVE);
      try
      {
        return f.getCanonicalPath();
      }
      catch (Throwable t)
      {
        return f.getAbsolutePath();
      }
    }
    else
    {
      String separator = desc.isWindows() ? "\\" : "/";
      return desc.getInstancePath() + separator + Installation.BACKUPS_PATH_RELATIVE;
    }
  }

  @Override
  public void toBeDisplayed(boolean visible)
  {
    if (visible && backupDirectoryInitialized)
    {
      refreshList();
    }
  }
}
