/**
 * GUI Commands
 * Copyright 2004 Andrew Pietsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id: DefaultFaceBuilder.java,v 1.31 2006/02/26 00:59:05 pietschy Exp $
 */

package org.pietschy.command;

import org.pietschy.command.log.Logger;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.swing.*;
import java.awt.*;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides the default implementation of {@link AbstractFaceBuilder}.
 */
public class
DefaultFaceBuilder
extends AbstractFaceBuilder
{
   static final String _ID_ = "$Id: DefaultFaceBuilder.java,v 1.31 2006/02/26 00:59:05 pietschy Exp $";

   private Logger log = CommandManager.getLogger(DefaultFaceBuilder.class);

   private Pattern acceleratorPattern = Pattern.compile("(.*)\\[(.*)\\]");

   /**
    * Creats a new builder.
    */
   public DefaultFaceBuilder()
   {
   }

   /**
    * <p>Creates a new empty face with the specified {@link FaceId id} and uses the specified
    * {@link CommandManager} for locating its parent.</p>
    *
    * @param faceManager the {@link FaceManager} the face belongs to.
    * @param id          the id of the face.
    */
   public Face
   createFace(FaceId id, FaceManager faceManager)
   {
      return new Face(id, faceManager);
   }

   /**
    * Implementers of this class are responsible for building a {@link Face} element for the
    * specified configuration data.
    *
    * @param face        the {@link Face} to configureMenu.
    * @param faceElement the element defining the faces properties.
    */
   public void
   configure(Face face, Element faceElement)
   {
      FaceId parentId = null;
      String extendsId = Util.getAttribute(faceElement, Names.EXTENDS_ATTRIBUTE);
      if (extendsId != null)
         parentId = FaceId.parseFaceReference(face.getId(), extendsId);
      face.setExtendsId(parentId);

      String menuToolTips = Util.getAttribute(faceElement, Names.MENU_TOOLTIPS_ATTRIBUTE);
      if (menuToolTips != null)
         face.setMenuTooltipEnabled(Names.ENABLED_VALUE.equals(menuToolTips));

      Element e = (Element) Util.getFirstElement(faceElement, Names.DESCRIPTION_ELEMENT);
      if (e != null)
      {
         face.setDescription(isEmptyElement(e) ? null : getElementText(e).trim());
      }

      e = (Element) Util.getFirstElement(faceElement, Names.LONG_DESCRIPTION_ELEMENT);
      if (e != null)
      {
         face.setLongDescription(isEmptyElement(e) ? null : getElementText(e).trim());
      }

      buildText(face, faceElement);

      // the text may have contained an accelerator so we check first..
      if (face.getAccelerator() == null)
      {
         buildAccelerator(face, faceElement);
      }
      buildIcons(face, faceElement);
      populateClientProperties(face, faceElement);
   }


   /**
    * Configures the text properties of the face from the specified face element.
    *
    * @param face        the {@link Face} to configureMenu.
    * @param faceElement the element defining the faces properties.
    */
   protected void
   buildText(Face face, Element faceElement)
   {
      List elements = Util.getElementsByName(faceElement, Names.TEXT_ELEMENT);

      boolean textConfigured = false;

      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element textElement = (Element) iter.next();

         if (isIncluded(textElement))
         {

            barfIfAlreadyConfigured(textConfigured, textElement);
            textConfigured = true;

            if (isEmptyElement(textElement))
            {
               face.setText(null);
               face.setMnemonic(null);
               face.setMnemonicIndex(null);
               face.setTextInherited(false);
            }
            else
            {
               String faceText = getElementText(textElement);

               Matcher matcher = acceleratorPattern.matcher(faceText);

               if (matcher.matches())
               {
                  faceText = matcher.group(1).trim();
                  String acceleratorText = matcher.group(2).trim();
                  face.setAccelerator(KeyStroke.getKeyStroke(acceleratorText));
               }

               face.setText(faceText);

               String horizontalPosition = getAttribute(textElement, Names.HORIZONTAL_TEXT_POSITION_ATTRIBUTE);

               if (horizontalPosition != null)
               {
                  if (Names.TEXT_POSITION_LEADING.equals(horizontalPosition.trim()))
                  {
                     face.setHorizontalTextPosition(new Integer(SwingConstants.LEADING));
                  }
                  else if (Names.TEXT_POSITION_TRAILING.equals(horizontalPosition.trim()))
                  {
                     face.setHorizontalTextPosition(new Integer(SwingConstants.TRAILING));
                  }
                  else if (Names.TEXT_POSITION_CENTRE.equals(horizontalPosition.trim()))
                  {
                     face.setHorizontalTextPosition(new Integer(SwingConstants.CENTER));
                  }
               }

               String verticalPsoition = getAttribute(textElement, Names.VERTICAL_TEXT_POSITION_ATTRIBUTE);

               if (verticalPsoition != null)
               {
                  if (Names.TEXT_POSITION_TOP.equals(verticalPsoition.trim()))
                  {
                     face.setVerticalTextPosition(new Integer(SwingConstants.TOP));
                  }
                  else if (Names.TEXT_POSITION_BOTTOM.equals(verticalPsoition.trim()))
                  {
                     face.setVerticalTextPosition(new Integer(SwingConstants.BOTTOM));
                  }
                  else if (Names.TEXT_POSITION_CENTRE.equals(verticalPsoition.trim()))
                  {
                     face.setVerticalTextPosition(new Integer(SwingConstants.CENTER));
                  }
               }

               String mnemonicString = getAttribute(textElement, Names.MNEMONIC_ATTRIBUTE);


               if (mnemonicString != null)
               {
                  if (mnemonicString.length() != 1)
                     throw new RuntimeException("Mnemonic '" + mnemonicString + "' in face " + face.getId() + " is not a single character");

                  KeyStroke ks = KeyStroke.getKeyStroke(mnemonicString.toUpperCase());
                  if (ks != null)
                     face.setMnemonic(new Integer(ks.getKeyCode()));
               }

               String mnemonicIndexString = getAttribute(textElement, Names.MNEMONIC_INDEX_ATTRIBUTE);
               try
               {
                  face.setMnemonicIndex(new Integer(mnemonicIndexString));
               }
               catch (NumberFormatException e1)
               {
               }
            }
         }
      }
   }

   private void
   barfIfAlreadyConfigured(boolean textConfigured, Element textElement)
   {
      if (textConfigured)
         throw new RuntimeException("Element configured twice:" + getElementPath(textElement));
   }

   /**
    * Scans the specified face element, extracts all the faces specified and adds them to
    * the face.
    *
    * @param face        the {@link Face} to configureMenu.
    * @param faceElement the element defining the faces properties.
    */
   protected void
   buildIcons(Face face, Element faceElement)
   {
      boolean iconConfigured = false;

      List elements = Util.getElementsByName(faceElement, Names.SMALL_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();

         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setIcon(null);
               face.setIconInherited(false);
            }
            else
            {
               face.setIcon(loadIcon(iconElement));
            }
         }
      }

      iconConfigured = false;
      elements = Util.getElementsByName(faceElement, Names.SELECTED_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();
         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setSelectedIcon(null);
               face.setSelectedIconInherited(false);
            }
            else
            {
               face.setSelectedIcon(loadIcon(iconElement));
            }
         }
      }

      iconConfigured = false;
      elements = Util.getElementsByName(faceElement, Names.ROLLOVER_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();

         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setRolloverIcon(null);
               face.setRolloverIconInherited(false);
            }
            else
            {
               face.setRolloverIcon(loadIcon(iconElement));
            }
         }

      }

      iconConfigured = false;
      elements = Util.getElementsByName(faceElement, Names.ROLLOVER_SELECTED_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();

         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setRolloverSelectedIcon(null);
               face.setRolloverSelectedIconInherited(false);
            }
            else
            {
               face.setRolloverSelectedIcon(loadIcon(iconElement));
            }
         }

      }

      iconConfigured = false;
      elements = Util.getElementsByName(faceElement, Names.PRESSED_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();
         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setPressedIcon(null);
               face.setPressedIconInherited(false);
            }
            else
            {
               face.setPressedIcon(loadIcon(iconElement));
            }
         }

      }

      iconConfigured = false;
      elements = Util.getElementsByName(faceElement, Names.DISABLED_ICON_ELEMENT);
      for (Iterator iter = elements.iterator(); iter.hasNext();)
      {
         Element iconElement = (Element) iter.next();
         if (iconElement != null && isIncluded(iconElement))
         {
            barfIfAlreadyConfigured(iconConfigured, iconElement);
            iconConfigured = true;

            if (isEmptyElement(iconElement))
            {
               face.setDisabledIcon(null);
               face.setDisabledIconInherited(false);
            }
            else
            {
               face.setDisabledIcon(loadIcon(iconElement));
            }
         }
      }

      Element textGap = (Element) Util.getFirstElement(faceElement, Names.ICON_TEXT_GAP_ELEMENT);
      if (textGap != null)
      {
         if (isEmptyElement(textGap))
         {
            face.setIconTextGap(null);
            face.setIconTextGapInherited(false);
         }
         else
         {
            face.setIconTextGap(new Integer(Util.getElementText(textGap).trim()));
         }
      }

   }


   /**
    * Loads the icon specified by the element.
    *
    * @param iconElement
    */
   protected Icon
   loadIcon(Element iconElement)
   {
      Icon icon = null;
      String type = getAttribute(iconElement, Names.TYPE_ATTRIBUTE);
      String value = getElementText(iconElement);

      ClassLoader classLoader = getCommandManager().getClassLoader();

      try
      {
         if (Names.TYPE_FILE.equals(type))
         {
            icon = new ImageIcon(value);
         }
         else if (Names.TYPE_URL.equals(type))
         {
            try
            {
               icon = new ImageIcon(new URL(value));
            }
            catch (MalformedURLException e)
            {
               throw new IconMissingException(value, type, e);
            }
         }
         else if (Names.TYPE_CLASSPATH.equals(type))
         {
            icon = new ImageIcon(classLoader.getResource(value));
         }
         else if (Names.TYPE_BEAN.equals(type))
         {
            try
            {
               icon = (Icon) classLoader.loadClass(value).newInstance();
            }
            catch (Exception e)
            {
               throw new IconMissingException(value, type, e);
            }
         }
         else if (Names.TYPE_FACTORY.equals(type))
         {
            try
            {
               // Read the class, method and parameters to invoke.  This patch
               // contributed by Daniel Frey.
               String[] parts = value.replaceAll(" ", "").split("#|\\(|,|\\)");
               Class factoryClass = classLoader.loadClass(parts[0]);
               Class[] arguments = new Class[parts.length - 2];
               String[] values = new String[parts.length - 2];
               for (int i = 0; i < arguments.length; i++)
               {
                   arguments[i] = String.class;
                   values[i] = parts[i + 2];
               }
               Method factoryMethod = factoryClass.getMethod(parts[1], arguments);
               icon = (Icon) factoryMethod.invoke(null, values);
            }
            catch (Exception e)
            {
               throw new IconMissingException(value, type, e);
            }
         }
         else if (Names.TYPE_ICON_FACTORY.equals(type))
         {
            IconFactory factory = getCommandManager().getIconFactory();

            if (factory == null)
               throw new NullPointerException("IconFactory is null.");

            try
            {
               icon = factory.createIcon(value);
            }
            catch (Exception e)
            {
               throw new IconMissingException(value, Names.TYPE_ICON_FACTORY, e);
            }
         }
         else
         {
            throw new RuntimeException("Icon type unspecified");
         }
      }
      catch (Exception e)
      {
         throw new RuntimeException("Failed to load icon for type=\"" + type + "\": " + value, e);
      }

      return icon;
   }

   /**
    * Interprets the accelerator string for the specified command and returns
    * a representative KeyStroke.<p>
    * The property form for the accelerator is as follows.<br>
    * <code>commandId.accel=alt-shift-ctrl-meta-c</code>
    */
   protected void
   buildAccelerator(Face face, Element commandElement)
   {
      boolean acceleraterConfigured = false;

      List elements = Util.getElementsByName(commandElement, Names.ACCELERATOR_ELEMENT);

      if (elements.size() == 0)
      {
         face.setAccelerator(null);
         face.setAcceleratorInherited(true);
      }
      else
      {
         for (Iterator iter = elements.iterator(); iter.hasNext();)
         {
            Element acceleratorElement = (Element) iter.next();

            if (isIncluded(acceleratorElement))
            {

               barfIfAlreadyConfigured(acceleraterConfigured, acceleratorElement);
               acceleraterConfigured = true;

               if (isEmptyElement(acceleratorElement))
               {
                  face.setAccelerator(null);
                  face.setAcceleratorInherited(false);
               }
               else
               {

                  String useDefaultValue = getAttribute(acceleratorElement, Names.USE_DEFAULT_MODIFIER_ATTRIBUTE);
                  boolean useDefault = useDefaultValue != null && "true".equals(useDefaultValue.toLowerCase());

                  NodeList metas = acceleratorElement.getElementsByTagName(Names.MODIFIER_ELEMENT);

                  int modifiers = 0;
                  // use later to determine the mechanism for using modifiers.
                  boolean hasModifiers = useDefault || metas.getLength() > 0;

                  if (useDefault)
                     modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();

                  for (int i = 0; i < metas.getLength(); i++)
                  {
                     Element node = (Element) metas.item(i);
                     String token = getAttribute(node, Names.MODIFIER_VALUE_ATTRIBUTE);
                     if (token.equalsIgnoreCase(Names.MODIFIER_ALT_VALUE))
                     {
                        modifiers |= Event.ALT_MASK;
                     }
                     else if (token.equalsIgnoreCase(Names.MODIFIER_CONTROL_VALUE))
                     {
                        modifiers |= Event.CTRL_MASK;
                     }
                     else if (token.equalsIgnoreCase(Names.MODIFIER_META_VALUE))
                     {
                        modifiers |= Event.META_MASK;
                     }
                     else if (token.equalsIgnoreCase(Names.MODIFIER_SHIFT_VALUE))
                     {
                        modifiers |= Event.SHIFT_MASK;
                     }
                     else if (token.equalsIgnoreCase(Names.MODIFIER_DEFAULT_VALUE))
                     {
                        // we are to use the default key so we ignore all other settings.
                        modifiers |= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
                     }
                  }


                  String keyStroke = getAttribute(acceleratorElement, Names.KEYSTROKE_ATTRIBUTE);
                  if (keyStroke != null)
                  {
                     // the keystroke attribute is used when the whole accelerator is specified using
                     // the syntax of KeyStroke.getKeyStroke(String)
                     if (hasModifiers)
                        throw new IllegalStateException("Modifiers not permitted when using the keyStroke attribute.");

                     KeyStroke ks = KeyStroke.getKeyStroke(keyStroke);

                     if (ks == null)
                        throw new IllegalArgumentException("'" + keyStroke + "' isn't a valid KeyStroke specification");

                     face.setAccelerator(ks);
                  }
                  else
                  {
                     String key = getAttribute(acceleratorElement, Names.KEY_ATTRIBUTE);
                     if (key == null || key.length() < 1)
                     {
                        throw new RuntimeException("Accelerator key or keyStroke not specified.");
                     }
                     else if (key.length() > 1)
                     {
                        // handle the case of keys like "f1" or "delete" etc
                        KeyStroke ks = KeyStroke.getKeyStroke(key.toUpperCase());
                        face.setAccelerator(KeyStroke.getKeyStroke(ks.getKeyCode(), modifiers));
                     }
                     else
                     {
                        // handle the case of single characters like 't' etc.
                        char character = key.toUpperCase().charAt(0);
                        KeyStroke ks = KeyStroke.getKeyStroke(character, modifiers);
                        face.setAccelerator(ks);
                     }
                  }
               }
            }
         }
      }
   }

   protected void
   populateClientProperties(Face face, Element faceRoot)
   {
      NodeList elements = faceRoot.getElementsByTagName(Names.CLIENT_PROPERTY_ELEMENT);
      for (int i = 0; i < elements.getLength(); i++)
      {
         Element e = (Element) elements.item(i);
         face.putClientProperty(getAttribute(e, Names.NAME_ATTRIBUTE), getElementText(e).trim());
      }
   }


}
