/*
 * $Id: ComponentPathBuilder.java 3888 2008-07-27 14:45:45Z uckelman $
 *
 * Copyright (c) 2004 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package VASSAL.tools;

import java.util.ArrayList;
import java.util.List;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;

/**
 * Provides an XPath-like syntax for identifying configuration components
 */
public class ComponentPathBuilder {

  private static ComponentPathBuilder instance;

  public static ComponentPathBuilder getInstance() {
    if (instance == null) {
      instance = new ComponentPathBuilder();
    }
    return instance;
  }


  /**
   * Return a string identifying the specified {@link Configurable}
   * components as a paththrough the configuration parent-child hierarchy.
   *
   * @param targetPath
   * @return
   */
  public String getId(Configurable[] targetPath) {
    SequenceEncoder se = new SequenceEncoder('/');
    for (int i = 0; i < targetPath.length; ++i) {
      String name = targetPath[i].getConfigureName();
      SequenceEncoder se2 = new SequenceEncoder(targetPath[i].getClass().getName(), ':');
      if (name != null) {
        se2.append(name);
      }
      se.append(se2.getValue());
    }
    return se.getValue() == null ? "" : se.getValue();
  }

  /**
   * Return a list of {@link Configurable} components specified by the
   * given identifier.
   *
   * @param id
   * @return
   * @throws PathFormatException if no such component exists
   */
  public Configurable[] getPath(String id) throws PathFormatException {
    final ArrayList<Configurable> list = new ArrayList<Configurable>();
    if (id.length() > 0) {
      SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(id, '/');
      addToPath(GameModule.getGameModule(), st, list);
    }
    return list.toArray(new Configurable[list.size()]);
  }

  private void addToPath(Configurable parent,
                         SequenceEncoder.Decoder st,
                         List<Configurable> path)
                         throws PathFormatException {
    if (st.hasMoreTokens()) {
      String id = st.nextToken();
      String name = null;
      SequenceEncoder.Decoder st2 = new SequenceEncoder.Decoder(id, ':');
      String className = st2.nextToken();
      if (st2.hasMoreTokens()) {
        name = st2.nextToken();
      }
      Configurable[] children = parent.getConfigureComponents();
      Configurable match = null;
      ArrayList<Configurable> partialMatches = new ArrayList<Configurable>();
      int i = -1;
      while (++i < children.length) {
        if (className.equals(children[i].getClass().getName())) {
          partialMatches.add(children[i]);
          if (name == null ? children[i].getConfigureName() == null
              : name.equals(children[i].getConfigureName())) {
            match = children[i];
            break;
          }
        }
      }
      if (match != null) {
        path.add(match);
        addToPath(match, st, path);
      }
      else if (!partialMatches.isEmpty()) {
        if (!st.hasMoreTokens()) {
           path.add(partialMatches.get(0));
        }
        else {
          ArrayList<Configurable> subPath = null;
          for (Configurable candidate : partialMatches) {
            ArrayList<Configurable> l = new ArrayList<Configurable>();
            try {
              addToPath(candidate, st.copy(), l);
              subPath = l;
// FIXME: adding to front of an ArrayList! Should we use LinkedList instead?
              subPath.add(0, candidate);
              break;
            }
            catch (PathFormatException e) {
              // No match found here.  Continue
            }
          }
          if (subPath != null) {
            path.addAll(subPath);
          }
          else {
            findFailed(className, name, parent);
          }
        }
      }
      else {
        findFailed(className, name, parent);
      }
    }
  }

  private void findFailed(String className, String name, Configurable parent)
    throws PathFormatException {

    String msgName = name;
    if (msgName == null) {
      msgName = className.substring(className.lastIndexOf('.') + 1);
    }
    throw new PathFormatException("Could not find " + msgName + " in " +
      VASSAL.configure.ConfigureTree.getConfigureName(parent.getClass()));
  }

  public static class PathFormatException extends Exception {
    private static final long serialVersionUID = 1L;

    public PathFormatException(String message) {
      super(message);
    }
  }
}
