/**
 * 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: ConfigurationManager.java,v 1.15 2006/02/26 00:59:05 pietschy Exp $
 */

package org.pietschy.command;

import org.pietschy.command.log.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

class
ConfigurationManager
{
   static final String _ID_ = "$Id: ConfigurationManager.java,v 1.15 2006/02/26 00:59:05 pietschy Exp $";

   private static final Logger log = CommandManager.getLogger(ConfigurationManager.class);

   private static final String ROOT_PATH = "//commands";
   public static final String COMMAND_ELEMENT = "command";
   public static final String GROUP_ELEMENT = "group";
   public static final String TOGGLE_GROUP_ELEMENT = "toggle-group";
   public static final String FACE_DEF_ELEMENT = "face-def";
   public static final String COMMAND_PATH = ROOT_PATH + "/" + COMMAND_ELEMENT;
   public static final String GROUP_PATH = ROOT_PATH + "/" + GROUP_ELEMENT;
   public static final String TOGGLE_GROUP_PATH = ROOT_PATH + "/" + TOGGLE_GROUP_ELEMENT;

//   private Element rootElement;
   private HashMap commandElements = new HashMap();
   private HashMap groupElements = new HashMap();

   private CommandManager commandManager;
   private FaceManager faceManager;
   private BuilderRegistry commandBuilders;
   private DocumentBuilder builder;


   protected ConfigurationManager(CommandManager manager, FaceManager faceManager,
                                  BuilderRegistry commandBuilders)
   {
      this.commandManager = manager;
      this.faceManager = faceManager;
      this.commandBuilders = commandBuilders;

      try
      {
         builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      }
      catch (ParserConfigurationException e)
      {
         throw new RuntimeException(e);
      }

      builder.setEntityResolver(new EntityResolver()
      {
         public InputSource resolveEntity(String publicId, String systemId)
         {
            if (systemId.endsWith("commands.dtd"))
               return getInputSource(systemId);
            else
               return null;
         }
      });

   }


   /**
    * Loads the command definitions from the specified File using the specified {@link Locale}.
    *
    * @throws LoadException is thrown if there is a problem.  This exception will wrap any
    *                       Exceptions thrown during the load process.
    */
   public void
   load(File file, Locale locale)
   throws LoadException
   {
      if (file == null)
         throw new NullPointerException("file is null");

      try
      {
         load(builder.parse(file), locale);
      }
      catch (Exception e)
      {
         throw new LoadException("Error reading file " + file + ": " + e.getMessage(), e);
      }
   }


   /**
    * Loads the command definitions from the specified URL using the specified {@link Locale}.
    *
    * @throws LoadException is thrown if there is a problem.  This exception will wrap any
    *                       Exceptions thrown during the load process.
    */
   public void
   load(URL url, Locale locale)
   throws LoadException
   {
      if (url == null)
         throw new NullPointerException("URL is null");

      try
      {
         load(builder.parse(url.toExternalForm()), locale);
      }
      catch (Exception e)
      {
         throw new LoadException("Error reading URL " + url + ": " + e.getMessage(), e);
      }
   }


   /**
    * Loads the command definitions from the specified reader using the specified {@link Locale}.
    *
    * @throws LoadException is thrown if there is a problem.  This exception will wrap any
    *                       Exceptions thrown during the load process.
    */
   public void load(InputStream in, Locale locale)
   throws LoadException
   {
      if (in == null)
         throw new NullPointerException("input stream is null");

      try
      {
         load(builder.parse(in), locale);
      }
      catch (Exception e)
      {
         throw new LoadException("Error reading inputstream: " + e.getMessage(), e);
      }
   }

   /**
    * Loads the command and group details from the specified document
    */
   private void
   load(Document document, Locale locale)
   {
      log.enter("load");

      Element documentRoot = (Element) Util.selectSingleNode(document, ROOT_PATH);

      // if this is the first load, save the configuration tree as is..
      // this is our second so we copy the important bits into our existing
      // root document.
      List elements = Util.selectNodes(document, COMMAND_PATH);
      for (int i = 0; i < elements.size(); i++)
      {
         Element element = (Element) elements.get(i);
         commandElements.put(Util.getElementId(element), element);
      }

      elements = Util.selectNodes(document, GROUP_PATH);
      for (int i = 0; i < elements.size(); i++)
      {
         Element element = (Element) elements.get(i);
         groupElements.put(Util.getElementId(element), element);
      }

      elements = Util.selectNodes(document, TOGGLE_GROUP_PATH);
      for (int i = 0; i < elements.size(); i++)
      {
         Element element = (Element) elements.get(i);
         groupElements.put(Util.getElementId(element), element);
      }


      List faceParents = findFaceParents(document, locale);
      for (int i = 0; i < faceParents.size(); i++)
      {
         faceManager.extractFaces((Element) faceParents.get(i));
      }

      // find all class elements whose parent is a command.
      String xpath = "//class/parent::command";
      List commands = Util.selectNodes(documentRoot, xpath);
      for (Iterator iter = commands.iterator(); iter.hasNext();)
      {
         Element element = (Element) iter.next();
         String id = element.getAttribute(Names.ID_ATTRIBUTE);
         Element classElement = (Element) Util.getFirstElement(element, Names.CLASS_ELEMENT);
         String className = Util.getElementText(classElement);
         Command c = instantiateCommand(id, className);
         c.export();
      }

      log.exit("load()");
   }

   private Locale
   getDefaultDocumentLocale(Document document)
   {
      Element documentRoot = (Element) Util.selectSingleNode(document, ROOT_PATH);
      String defaultLocale = Util.getAttribute(documentRoot, "defaultLocale");

      if (defaultLocale == null)
         return Locale.getDefault();

      String[] bits = defaultLocale.split("_");

      switch (bits.length)
      {
         case 0:
            throw new RuntimeException("defaultLocale is empty");
         case 1:
            return new Locale(bits[0]);
         case 2:
            return new Locale(bits[0], bits[1]);
         case 3:
            return new Locale(bits[0], bits[1], bits[2]);
         default:
            throw new RuntimeException("Illegal defaultLocale: " + defaultLocale);
      }

   }

   private List
   findFaceParents(Document document, Locale local)
   {
      log.enter("findFaceParents");
      String faceParents;

      Locale documentDefaultLocal = getDefaultDocumentLocale(document);
      if (documentDefaultLocal.equals(local))
         faceParents = "//face[count(@locale) = 0 or @locale='" + local.toString() + "']/parent::*";
      else
         faceParents = "//face[@locale='" + local.toString() + "']/parent::*";

      log.debug("face parent xpath: " + faceParents);

      List parents = Util.selectNodes(document, faceParents);

      log.exit("findFaceParents");
      return parents;
   }

   protected Command
   instantiateCommand(String commandId, String className)
   {
      try
      {
         Class c = commandManager.getClassLoader().loadClass(className);
         Constructor ctor = c.getConstructor(new Class[]{CommandManager.class, String.class});
         Object command = ctor.newInstance(new Object[]{commandManager, commandId});

         return (Command) command;
      }
      catch (Exception e)
      {
         throw new RuntimeException("Unable to instantiate command '" + commandId +
         "' using class '" +
         className +
         "'", e);
      }
   }

   public CommandGroup
   instantiateGroup(String groupId, GroupFactory groupFactory)
   {
      // if the class is specified by the user we create it directly..
      Element groupElement = getGroupElement(groupId);

      if (groupElement == null)
         return null;

      String groupClass = getGroupClass(groupElement);
      if (groupClass != null)
      {
         return (CommandGroup) instantiateCommand(groupId, groupClass);
      }
      else
      {
         if (isToggleGroup(groupId))
         {
            return groupFactory.createToggleGroup(commandManager, groupId);
         }
         else
         {
            return groupFactory.createGroup(commandManager, groupId);
         }
      }
   }


   protected String
   getGroupClass(Element groupElement)
   {
      Element classElement = (Element) Util.getFirstElement(groupElement, Names.CLASS_ELEMENT);
      return Util.getElementText(classElement);
   }

   protected String
   getCommandClass(Element commandElement )
   {
      Element classElement = (Element) Util.getFirstElement(commandElement, Names.CLASS_ELEMENT);
      return Util.getElementText(classElement);
   }

   /**
    * Returns the class of the specified command as specified by the configuration file.  If the
    * class wasn't specified, this method returns null.
    *
    * @param command
    * @return the class of the specified command as specified by the configuration file, or null if
    * it wasn't specified.
    */
   public String
   getSpecifiedClass(Command command)
   {
      if (command instanceof CommandGroup)
      {
         Element groupElement = getGroupElement(command.getId());
         return groupElement != null ? getGroupClass(groupElement) : null;
      }
      else
      {
         Element commandElement = getCommandElement(command.getId());
         return commandElement != null ? getCommandClass(commandElement) : null;
      }
   }

   private InputSource
   getInputSource(String systemId)
   {
      try
      {
         URL url = new URL(systemId);
         return new InputSource(url.openStream());
      }
      catch (Exception e)
      {
      }

      try
      {
         FileInputStream fis = new FileInputStream(systemId);
         return new InputSource(fis);
      }
      catch (Exception e)
      {
      }

      return new InputSource(getClass().getResourceAsStream("/org/pietschy/command/commands.dtd"));
   }

   /**
    * Gets the {@link Element} describing the group for the specified group Id.
    *
    * @param id the id of the group.
    * @return the {@link Element} for the specified group or <tt>null</tt> if it doesn't exist.
    */
   protected Element
   getGroupElement(String id)
   {
      return (Element) groupElements.get(id);
   }

   /**
    * Gets the {@link Element} describing the command for the specified command Id.
    *
    * @param id the id of the command.
    * @return the {@link Element} for the specified command or <tt>null</tt> if it doesn't exist.
    */
   private Element
   getCommandElement(String id)
   {
      return (Element) commandElements.get(id);
   }

   /**
    * Checks if the specified commandId is for a {@link CommandGroup}.
    *
    * @param commandId the id to check.
    * @return <tt>true</tt> if the id is for a group, <tt>false</tt> otherwise.
    */
   public boolean
   isGroup(String commandId)
   {
      return groupElements.containsKey(commandId);
   }

   public boolean
   isToggleGroup(String groupId)
   {
      Element element = getGroupElement(groupId);
      return element != null && Names.TOGGLE_GROUP.equals(element.getTagName());
   }

   public void
   configure(Command command)
   {
      Element element = null;
      String commandId = command.getId();

      boolean isGroup = command instanceof CommandGroup;

      if (isGroup)
         element = getGroupElement(commandId);
      else
         element = getCommandElement(commandId);

      // if this command has no configuration data then we do nothing, this is
      // typical for programatically configured commands.
      if (element == null)
         return;

      // load any registered faces.
      Face[] faces = faceManager.getFacesFor(command);
      for (int i = 0; i < faces.length; i++)
         command.installFace(faces[i]);

      // and get the appropriate builder and configureMenu the command.
      AbstractCommandBuilder configurator = commandBuilders.get(command.getClass());
      configurator.configure(command, element);
   }


}
