/*
 * EditPanel.java
 *
 * Copyright 2011 John W Dawson
 *
 * This code is distributed under the terms of the GNU General Public License, version 3
 *
 * This class implements the tabbed panel used to display and edit entries
 */

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.regex.*;
import java.util.logging.*;
import javax.naming.directory.*;
public class EditPanel extends JTabbedPane implements ListSelectionListener, DocumentListener, FocusListener
{
  static Logger log = Logger.getLogger ("LdapAddressBook");
  private LdapSearchList searchList;
  private MessageLine messageLine;
  private JTextField searchBox;
  private ControlPanel controlPanel;
  private Map<String, AddressBookField> fieldList = new HashMap<String, AddressBookField> ();
  private ArrayList<DnField> dnFields = new ArrayList<DnField> ();
  private static Pattern fieldPattern = Pattern.compile ("%(\\w+)%");
  private static Pattern dnSpCharPattern = Pattern.compile ("[,\\=\\+<>;\\\"\\\\]");
  private String displayFields;
  private String baseDN;
  private SearchResult currentEntry = null;
  private Boolean updated = false;
  private Boolean valid = true;
  private AddressBookField currentField;
  private LdapConnection connection;
  private ConfigurationRecord configuration;
  
  private class DnField
  {
    private ArrayList<String> objectClasses = new ArrayList<String> ();
    private AddressBookField field;
    
    public DnField (ConfigurationRecord fieldDef, Boolean terminal)
    {
      this.field = fieldList.get (fieldDef.getElementValue ("field"));
      this.field.setDnField (terminal);
      for (ConfigurationRecord objectClass : fieldDef.getElementRecords ("objectclass"))
      {
        objectClasses.add (objectClass.getValue ());
      }
    }
    
    public void appendText (String text)
    {
      this.field.setText (this.field.getText() + text);
    }    
    
    public void prependDnString (StringBuffer buff)
    {
      // First need to escape any special characters in field value
      StringBuffer valueBuff = new StringBuffer ();
      Matcher matcher = dnSpCharPattern.matcher (field.getText ());
      while (matcher.find ())
      {
        matcher.appendReplacement (valueBuff, "\\\\" + 
                                     (matcher.group (0).equals("\"") 
                                        ? "22" 
                                        : matcher.group (0).equals("\\")
                                            ? "\\\\\\\\"
                                            : matcher.group (0)));
      }
      matcher.appendTail (valueBuff);
      buff.insert (0, field.getLdapAttribute () + "=" + valueBuff.toString () + (buff.length() > 0 ? "," : ""));
    }
    
    public Attributes getMatchingAttribute (String suffix)
    {
      return new BasicAttributes (field.getLdapAttribute (), field.getText () + suffix);
    }
    
    public void addObjectClasses (Attributes attributes)
    {
      Attribute attribute = new BasicAttribute ("objectClass");
      for (String objectClass : objectClasses)
      {
        attribute.add (objectClass);
      }
      attributes.put (attribute);
    }
    
    public void addToAttributes (Attributes attributes)
    { 
      attributes.put (field.getLdapAttribute (), field.getText());
    }
    
    public String getLabel ()
    {
      return field.getLabel ();
    }  
    
    public void setFocus ()
    {
      field.setFocus ();
    }
  }
  
  public void initialise (JTextField searchBox, ExtensionSelector extensionSelector, MessageLine messageLine, LdapSearchList searchList, 
                          ControlPanel controlPanel, LdapConnection connection, ConfigurationRecord configuration)
  {
    this.searchBox = searchBox;
    this.messageLine = messageLine;
    this.searchList = searchList;
    this.controlPanel = controlPanel;
    this.connection = connection;
    this.configuration = configuration;
    searchList.addListSelectionListener (this);
    
    // Store pattern for display fields
    displayFields = configuration.getElementValue ("displayfields");
    
    
    // Loop for all tabs
    for (ConfigurationRecord tabLayout : configuration.getElementRecords ("tab"))
    {
      // Create new tabbed panel
      AddressBookPanel panel = new AddressBookPanel ();
      
      // Loop for fields on tab
      for (ConfigurationRecord fieldDef : tabLayout.getElementRecords ("field"))
      {
        // Create an image/multi=line/text field as appropriate
        AddressBookField field;
        if (fieldDef.getBooleanValue ("imagefield"))
        {
          field = new AddressBookImageField (fieldDef, fieldList, configuration, messageLine);
        }
        else if (fieldDef.getIntegerValue ("numlines") > 1)
        {
          field = new AddressBookMultiLineField (fieldDef, fieldList);
        }
        else
        {
          field = new AddressBookTextField (fieldDef, extensionSelector, messageLine, fieldList);
        }

        // Add field to list          
        fieldList.put (fieldDef.getAttributeValue ("id"), field);
        
        // Add field to panel 
        panel.addField (fieldDef.getElementValue ("label"), field);
        
        // Attach to the field as a handler for entry in the field
        field.addDocumentListener (this);
        field.addFocusListener (this);
      }
      
      // Add the tab to the pane
      add (tabLayout.getAttributeValue ("title"), panel);
    }
    
    // Match all occurences of a field reference in the pattern for the display string
    Matcher matcher = fieldPattern.matcher (configuration.getElementValue ("displayfields"));
    while (matcher.find())
    {        
      // Check the referenced field has been defined in configuration
      if (fieldList.get (matcher.group (1)) == null)
      {
        JOptionPane.showMessageDialog 
          (null, "The field \"" + matcher.group (1) + "\" is not defined in the configuration file",
           "Invalid Configuration Data", JOptionPane.ERROR_MESSAGE);
        System.exit (0);
      }
    }
    
    // Store base DN & DN field refs
    baseDN = configuration.getElementValue ("ldapbasedn");
    Boolean terminal = true;
    for (ConfigurationRecord dnField : configuration.getElementRecords ("dnfield"))
    {
      dnFields.add (new DnField (dnField, terminal));
      terminal = false;
    }
    
    // Initialise all the fields 
    for (AddressBookField field : fieldList.values ())
    {
      field.initialise ();
    }
  }
  
  public void enableFields (Boolean enable)
  {
    // Loop for all fields
    for (AddressBookField field : fieldList.values ())
    {
      // Enable the field
      field.setEnabled (enable);
    }
  }
  
  private void displayEntry (SearchResult entry)
  {
 
    // Check if there is an entry to display
    if (entry == null)
    {
      // If not clear all the fields
      clear ();

      // Disable the new button      
      controlPanel.disableButton ("New");
      
    }
    else
    {
      // Loop for all fields
      for (AddressBookField field : fieldList.values ())
      {
        // Display value of this field from entry
        field.displayValue (entry);
        // Enable or disable any call button
        field.enableCallButton ();
              
      }
      
      // Enable the delete, copy and new buttons if modifications allowed
      if (configuration.getBooleanValue ("allowmodifications"))
      {
        controlPanel.enableButton ("New");
        controlPanel.enableButton ("Copy");
        controlPanel.enableButton ("Delete");
      }
    }
    
    // Select first field on first tab
    setSelectedIndex (0);
    ((Container)getSelectedComponent()).getComponent(1).requestFocusInWindow();
    
    // Disable the Save & Undo buttons
    controlPanel.disableButton ("Save");
    controlPanel.disableButton ("Undo");
    
    // Clear the flag
    updated = false;
    valid = true;
  }
  
  public void valueChanged (ListSelectionEvent e)
  {
    // Ignore multiple events generated during change
    if (!e.getValueIsAdjusting())
    {

      // Get the entry which has been selected
      SearchResult selectedEntry = (SearchResult) searchList.getSelectedValue();
      
      // Check if an entry has actually been selected
      if (selectedEntry != null)
      {
        // Confirm discarding of changes to any previous entry
        if (discardChanges ())
        {
          // Display this entry
          displayEntry (selectedEntry);
          currentEntry = selectedEntry;
        }
        else
        {
          // Deselect the entry
          searchList.clearSelection ();
        }
      
      }
    }
  }
  
  public void undo ()
  {
    // Confirm discarding of any unsaved changes
    if (discardChanges ())
    {
      // Reset all the fields to values from original entry
      displayEntry (currentEntry);
    }
    else
    {
      // Return focus to current field
      currentField.setFocus ();
    }
  }
  
  private Boolean discardChanges ()
  {
    // Prompt the user to confirm discarding any unsaved changes
    return !updated || (JOptionPane.showConfirmDialog 
                          (this, "Discard changes to " + displayName (),
                           "Discard Changes", JOptionPane.YES_NO_OPTION)
                                    == JOptionPane.YES_OPTION);
  }
  
  public void clear ()
  {
    // Loop for all fields
    for (AddressBookField field : fieldList.values ())
    {
      // Clear this field 
      field.clear ();
      
      // Disable any call button
      field.disableCallButton ();
      
    }
    
    // Select first field on first tab
    setSelectedIndex (0);
    ((Container)getSelectedComponent()).getComponent(1).requestFocusInWindow();
    
  }
      
  public void enableCallButtons ()
  {
    // Loop for all fields
    for (AddressBookField field : fieldList.values ())
    {
      // Enable any call button
      field.enableCallButton ();
      
    }
  }
  
  public void disableCallButtons ()
  {
    // Loop for all fields
    for (AddressBookField field : fieldList.values ())
    {
      // Disable any call button
      field.disableCallButton ();
      
    }
  }
  
  public void newEntry ()
  {
    // Check if OK to discard any unsaved changes
    if (discardChanges ())
    {
      // Clear all fields
      clear ();
      
      // Clear the search box
      searchBox.setText ("");
      
      // Clear the list of search results
      searchList.clear ();
      
      // Disable all buttons
      controlPanel.disableAllButtons ();

      // Clear flag indicating unsaved changes
      updated = false;
      
      // Set flag to indicate all fields are valid
      valid = true;
      
      currentEntry = null;
      
    }
    else
    {
      // Return focus to current field
      currentField.setFocus ();
    }
  }
    
  public void saveEntry (boolean copy)
  {
    // Check last field entered was valid
    if (valid)
    {
      // Create empty list of attributes
      Attributes attributes = new BasicAttributes ();
      
      // Loop for all fields
      for (AddressBookField field : fieldList.values ())
      {
        // Check required field has been entered
        if (field.requiredEmpty ())
        {
          return;
        }
        // Add field value to list of attributes (unless DN field)
        if (!field.isDnField())
        {
          // Get text value
          String textValue = field.getText();
          if (textValue == null)
          {
            // Not a text field, get binary data and add to attributes if not null
            if (field.getBinary() != null)
            {
              attributes.put (field.getLdapAttribute (), field.getBinary());
            }
          }
          // Otherwise add text value if not empty
          else if (textValue.length() > 0)
          {
            attributes.put (field.getLdapAttribute (), textValue);
          }
        }
      }
      
      messageLine.setTemporaryText ("Saving entry...");
    
      // Loop for all dn fields except terminal, starting with innermost
      StringBuffer dnBuff = new StringBuffer ();
      for (int pos = dnFields.size() - 1; pos > 0; --pos)
      {
        // Check corresponding entry exists in the directoru
        DnField dnField = dnFields.get (pos);
        Attributes dnAttributes = dnField.getMatchingAttribute ("");
        Boolean exists = connection.entryExists (dnBuff.toString (), dnAttributes);
        dnField.prependDnString (dnBuff);
        if (!exists)
        {
          // If not create the entry
          dnField.addObjectClasses (dnAttributes);
          if (!connection.createEntry (dnBuff.toString (), dnAttributes))
          {
            JOptionPane.showMessageDialog (null, "Error saving entry, see log file", 
                                           "LDAP save failed", JOptionPane.ERROR_MESSAGE);
            return;
          }
        }
        
      }
      String parentDn = dnBuff.toString ();
      
      // Get terminal dn field
      DnField terminal = dnFields.get (0);
            
      // If making a copy of an existing entry find a unique value for the terminal attribute
      if (copy)
      {
        String suffix = " (Copy)";
        for (int copyNum = 1; 
             connection.entryExists (parentDn, terminal.getMatchingAttribute (suffix));
             suffix = " (Copy " + ++copyNum + ")")
        {}
        
        // Update the terminal attribute
        terminal.appendText (suffix);
      }
      
      // Prepend terminal attribute to dn
      terminal.prependDnString (dnBuff);
      String entryDn = dnBuff.toString ();

      // Add terminal field to list of attributes  
      terminal.addToAttributes (attributes);    
      
      // If this is a new entry, or an update to an existing entry which changes its dn, need to check that
      // an entry with same dn does not already exist in directory
      if (!copy && (currentEntry == null || !entryDn.equals (currentEntry.getName())))
      {
        Attributes dnAttributes = terminal.getMatchingAttribute ("");
        if (connection.entryExists (parentDn, dnAttributes))
        {
          // Construct message explaining which fields need to be made unique
          StringBuffer messageText = new StringBuffer ("Please modify the " + dnFields.get(0).getLabel ());
          if (dnFields.size () == 1)
          {
            messageText.append (" field");
          }
          else
          {
            for (int pos = 1; pos < dnFields.size () - 1; ++pos)
            {
              messageText.append (", " + dnFields.get(pos).getLabel ());
            }
            messageText.append (" and/or " + dnFields.get(dnFields.size () - 1).getLabel () + " fields");
          }
          messageText.append (" so this entry is uniquely identified in the directory");  

          dnFields.get(0).setFocus ();            
          JOptionPane.showMessageDialog (null, messageText.toString (), "Duplicate Entry", JOptionPane.ERROR_MESSAGE);
          messageLine.clearTemporaryText ();
          return;
        }
      }
      
      // If updating an existing entry the original entry must be deleted
      if (currentEntry != null && !copy)
      {
        if (!connection.deleteEntry (currentEntry.getName ()))
        {
          messageLine.clearTemporaryText ();
          JOptionPane.showMessageDialog (null, "Error saving entry, see log file", 
                                         "LDAP save failed", JOptionPane.ERROR_MESSAGE);
          return;
        }
      }        
      
      // Add object classes
      terminal.addObjectClasses (attributes);
      
      // Insert the entry
      if (connection.createEntry (entryDn, attributes))
      {
      
        // Log the operation
        log.info ("Entry: " + displayName () + (currentEntry == null || copy ? " created" : " updated"));
        
        // Store name and attributes of new entry, and clear flag 
        currentEntry = new SearchResult (entryDn, null, attributes);
        updated = false;
        
        // Disable save and undo buttons
        controlPanel.disableButton ("Save");
        controlPanel.disableButton ("Undo");
        
        // Enable the delete & copy buttons      
        controlPanel.enableButton ("Delete");
        controlPanel.enableButton ("Copy");
              
        // Redo the current search
        searchList.search ();
  
        // Display message to confirm save      
        messageLine.setTransientText ("Entry: " + displayName () + " saved");
      }
      else
      {
        messageLine.clearTemporaryText ();
        JOptionPane.showMessageDialog (null, "Error saving entry, see log file", 
                                       "LDAP save failed", JOptionPane.ERROR_MESSAGE);
      }
    }
  }
  
  public void deleteEntry ()
  {
    // Get confirmation from user
    if (JOptionPane.showConfirmDialog 
          (null, "Do you really want to delete the entry: " + displayName () + "?", 
          "Confirm Delete", JOptionPane.YES_NO_OPTION)
                                    == JOptionPane.YES_OPTION) 
    {
      // Delete the entry
      if (connection.deleteEntry (currentEntry.getName ()))
      {
      
        // Log the operation
        log.info ("Entry: " + displayName () + " deleted");
        
        // Display message to confirm delete      
        messageLine.setTransientText ("Entry: " + displayName () + " deleted");
        
        // Clear all fields
        clear ();
        
        // Disable all buttons
        controlPanel.disableAllButtons ();
        
        // Clear entry & flag
        currentEntry = null;
        updated = false;
        
        // Clear the list of search results (temporarily disable list event handler so entry is not cleared
        searchList.removeListSelectionListener (this);
        searchList.clear ();
        searchList.addListSelectionListener (this);
      }      
      else
      {
        JOptionPane.showMessageDialog (null, "Error deleting entry, see log file", 
                                       "LDAP delete failed", JOptionPane.ERROR_MESSAGE);
      }
    }
  }
      
  public Map<String, AddressBookField> getFieldList ()
  {
    return fieldList;
  }
  
  private String displayName ()
  {
    StringBuffer buff = new StringBuffer ();
    
    // Loop for all fields referenced in the display fields pattern
    Matcher matcher = fieldPattern.matcher (displayFields);
    while (matcher.find ())
    {
      // Replace the field reference with the actual value of the field (if any) in this entry
      // (Need to escape backslashes)
      matcher.appendReplacement (buff, fieldList.get (matcher.group (1)).getText().replaceAll("\\\\","\\\\\\\\"));
    }
    
    // Add any remaining text
    matcher.appendTail (buff);
  
    return buff.toString ();
  }
  
  private void fieldUpdated ()
  {
    // Check if this is the first change to a new or saved entry 
    if (!updated)
    {
      // If so enable the New, Save & Undo buttons
      controlPanel.enableButton ("New");
      controlPanel.enableButton ("Save");
      controlPanel.enableButton ("Undo");
      
      // Set the flag
      updated = true;
    }
  }      
      
  public void insertUpdate(DocumentEvent event) 
  {
    fieldUpdated ();
  }
  
  public void removeUpdate(DocumentEvent event) 
  {
    fieldUpdated ();
  }
  public void changedUpdate(DocumentEvent event) 
  {}
  
  public void focusGained (FocusEvent e) 
  {
    currentField = (AddressBookField) e.getSource ();
  }
    
  public void focusLost (FocusEvent e) 
  {
    // Check where focus is moving from/to
    AddressBookField previous = (AddressBookField) e.getSource();
    Component next = e.getOppositeComponent ();
    Boolean nextIsField = fieldList.containsValue (next);
    Boolean nextIsCallButton = next instanceof CallButton;
    
    // If focus is going to another field, the search box, the save
    // button, the copy button or a call button then the field contents need to be validated
    if (nextIsField || next == searchBox ||
        controlPanel.isButton (next, "Save") || controlPanel.isButton (next, "Copy") || nextIsCallButton)
    {
      valid = previous.validateText ();
    }
    
    // If focus is moving to another field and previous field is valid then next field
    // is now the current field
    if (nextIsField)
    {
      if (valid)
      {
        currentField = (AddressBookField) next;
      }
    }
    else if (next == searchBox)
    {
      // If focus moving to search box clear current field
      currentField = null;
    }
    
  }
}