/*
 * $Id: ComponentI18nData.java 5136 2009-02-22 15:42:22Z uckelman $
 *
 * Copyright (c) 2000-2006 by Rodney Kinney, Brent Easton
 *
 * 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.i18n;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Configurable;
import VASSAL.counters.BasicPiece;
import VASSAL.counters.Decorator;
import VASSAL.counters.GamePiece;
import VASSAL.counters.PlaceMarker;

/**
 * Object encapsulating the internationalization information for a component.
 * The majority of translatable components subclass AbstractConfigurable,
 * but some extend JFrame or JDialog and implement Configurable or
 * AutoConfigurable.
 * 
 * AbstractConfigurable components are almost completely handled within the
 * AbstractConfigurable base class. AutoConfigurable/Configurable components
 * must call a different constructor and supply additional information.
 * 
 * @author Brent Easton
 */
public class ComponentI18nData {
  protected String prefix;
  protected Translatable parent;
  protected Configurable myComponent;
  protected Map<String, Property> translatableProperties =
    new TreeMap<String, Property>();
  protected Map<String, Property> allProperties =
    new TreeMap<String, Property>();
  protected List<Translatable> children = new ArrayList<Translatable>();

  /**
   * Build from an AbstractConfigurable. The parent will be set from
   * AbstractConfigurable.add(). untranslatedValues will be filled in as
   * attributes are translated.
   * 
   * @param c
   *          AbstractConfigurable component
   * @param prefix
   *          I18n Prefix
   */
  public ComponentI18nData(AbstractConfigurable c, String prefix) {
    init(c, prefix, c.getAttributeNames(), c.getAttributeTypes(), c.getAttributeDescriptions());
  }

  public ComponentI18nData(AbstractConfigurable c, String prefix, ArrayList<String> names, ArrayList<Class<?>> types, ArrayList<String> descriptions) {
    init(c, prefix, names.toArray(new String[0]), types.toArray(new Class<?>[0]), descriptions.toArray(new String[0]));
  }
  /**
   * Build from an AutoConfigurable
   * 
   * @param c
   *          AutoConfgurable component
   * @param prefix
   *          I18n prefix
   */
  public ComponentI18nData(AutoConfigurable c, String prefix) {
    this.prefix = prefix;
    parent = null;
    init(c, prefix, c.getAttributeNames(),
         c.getAttributeTypes(), c.getAttributeDescriptions());
  }

  protected void init(Configurable c, String pfx, String[] names,
                      Class<?>[] types, String[] descriptions) {
    boolean[] translatable = new boolean[types.length];
    for (int i = 0; i < types.length; i++) {
      translatable[i] = types[i] != null &&
        (types[i].equals(String.class) ||
         TranslatableConfigurerFactory.class.isAssignableFrom(types[i]));
    }
    init(c, pfx, names, descriptions, translatable);
  }

  protected void init(Configurable c, String pfx, String[] names, String[] descriptions, boolean[] translatable) {
    prefix = pfx;
    myComponent = c;
    for (Configurable child : myComponent.getConfigureComponents()) {
      children.add(child);
    }
    for (int i = 0; i < translatable.length; i++) {
      Property p = new Property(names[i], descriptions[i]);
      allProperties.put(names[i], p);
      if (translatable[i]) {
        translatableProperties.put(names[i], p);
      }
    }
  }

  /**
   * Build from a Configurable. Configurable does not support
   * getAttributeNames() getAttributeTypes() or getAttributeValueString(),
   * so more information must be supplied.
   * 
   * @param c
   *          Component
   * @param prefix
   *          I18n prefix
   * @param parent
   *          parent translatable
   * @param names
   *          Array of attribute names
   * @param translatable
   *          Array of Attribute translatable status
   */
  public ComponentI18nData(Configurable c, String prefix, Translatable parent, String[] names, boolean[] translatable, String[] descriptions) {
    myComponent = c;
    this.prefix = prefix;
    this.parent = parent;
    init(c, prefix, names, descriptions, translatable);
  }

  public ComponentI18nData(Configurable c, String prefix, Translatable parent) {
    this(c, prefix, parent, new String[0], new boolean[0], new String[0]);
  }

  public ComponentI18nData(Configurable c, String prefix) {
    this(c, prefix, (Translatable) null);
  }

  /**
   * Special build for PrototypeDefinition and PieceSlot
   */
  public ComponentI18nData(Configurable c, GamePiece piece) {
    myComponent = c;
    this.prefix = TranslatablePiece.PREFIX;
    parent = null;
    for (GamePiece p = piece; p != null;) {
      if (p instanceof TranslatablePiece) {
        PieceI18nData pieceData = ((TranslatablePiece) p).getI18nData();
        for (PieceI18nData.Property prop : pieceData.getProperties()) {
          Property property = new Property(prop.getName(), prop.getDescription());
          translatableProperties.put(prop.getName(), property);
          allProperties.put(prop.getName(),property);
        }
      }
      if (p instanceof PlaceMarker) {
        if (((PlaceMarker)p).isMarkerStandalone()) {
          children.add(new TranslatableMarker((PlaceMarker) p));
        }
      }
      if (p instanceof BasicPiece) {
        p = null;
      }
      else {
        p = ((Decorator) p).getInner();
      }
    }
  }

  /**
   * Return a unique Key prefix identifying this component
   */
  public String getPrefix() {
    return prefix;
  }

  public void setPrefix(String p) {
    prefix = p;
  }

  /**
   * Return a unique key prefix including a full path of parent prefixes. All Translatable Pieces share a common prefix.
   * 
   * @return
   */
  public String getFullPrefix() {
    if (TranslatablePiece.PREFIX.equals(prefix)) {
      return prefix;
    }
    String fullPrefix = getOwningComponent() == null ? "" : getOwningComponent().getI18nData() //$NON-NLS-1$
        .getFullPrefix();
    if (fullPrefix.length() > 0 && prefix.length() > 0) {
      fullPrefix += "."; //$NON-NLS-1$
    }
    return fullPrefix + prefix;
  }

  /**
   * Return a list of all of the translatable Keys for attributes of this Translatable item.
   */
  public Collection<String> getAttributeKeys() {
    return translatableProperties.keySet();
  }

  /**
   * Set the owning Translatable of this component
   */
  public void setOwningComponent(Translatable t) {
    parent = t;
  }

  /**
   * Return the owning Translatable of this component
   */
  public Translatable getOwningComponent() {
    return parent;
  }

  /**
   * Is the specified attribute allowed to be translated?
   * 
   * @param attr
   *          Attribute name
   * @return is translatable
   */
  public boolean isAttributeTranslatable(String attr) {
    return translatableProperties.containsKey(attr);
  }

  /**
   * Return true if this component has any translatable attributes, or if any of its children are translatable
   * 
   * @return component translatable status
   */
  public boolean isTranslatable() {
    if (translatableProperties.size() > 0) {
      return true;
    }
    for (Translatable child : children) {
      if (child.getI18nData().isTranslatable()) {
        return true;
      }
    }
    return false;
  }

  /**
   * Force a specified attribute to be translatable/not translatable
   * 
   * @param attribute
   *          Attribute name
   * @param set
   *          translatable status
   */
  public void setAttributeTranslatable(String attribute, boolean set) {
    if (set) {
      translatableProperties.put(attribute, allProperties.get(attribute));
    }
    else {
      translatableProperties.remove(attribute);
    }
  }

  /**
   * Convenience method to force all attributes to be not translatable
   */
  public void setAllAttributesUntranslatable() {
    translatableProperties.clear();
  }

  /**
   * Apply a translatation to the specified attribute. Record the untranslated value in the untranslatedValues array and
   * set the new value into the real attribute
   * 
   * @param attr
   *          Attribute name
   * @param value
   *          Translated value
   */
  public void applyTranslation(String attr, String value) {
    Property p = translatableProperties.get(attr);
    if (attr != null) {
      p.setUntranslatedValue(myComponent.getAttributeValueString(attr));
      myComponent.setAttribute(attr, value);
    }
  }

  /**
   * Return description for named Attribute
   * 
   * @param attr
   * @return description
   */
  public String getAttributeDescription(String attr) {
    return allProperties.get(attr).getDescription();
  }

  /**
   * Return the pre-translation value stored in this Object.
   * 
   * @param attr
   *          Attribute Name
   * @return untranslated value
   */
  public String getLocalUntranslatedValue(String attr) {
    String val;
    Property p = allProperties.get(attr);
    if (p == null || p.getUntranslatedValue() == null) {
      val = myComponent.getAttributeValueString(attr);
    }
    else {
      val = p.getUntranslatedValue();
    }
    return val;
  }

  /**
   * Set an untranslatedValue for the specified attribute. Used by components that do not subclass AbstractConfigurable
   * 
   * @param attr
   *          Attribute name
   * @param value
   *          untranslated value
   */
  public void setUntranslatedValue(String attr, String value) {
    allProperties.get(attr).setUntranslatedValue(value);
  }

  /*
   * Return a translation of an attribute
   */
  public String getTranslatedValue(String attr, Translation translation) {
    String fullKey = getFullPrefix() + attr;
    return translation.translate(fullKey);
  }

  /**
   * Return all child Translatable components of this component
   * 
   * @return Child translatables
   */
  public List<Translatable> getChildren() {
    return Collections.unmodifiableList(children);
  }

  /**
   * Return true if this component or any of its children have at least one translatable attribute with a non-null value
   * that does not have a translation in the supplied translation.
   * 
   * @param t
   *          Translation
   * @return true if translation of this component is not complete
   */
  public boolean hasUntranslatedAttributes(Translation t) {
    if (t == null) {
      return false;
    }
    /*
     * Check attributes of this component first
     */
    for (Property p : translatableProperties.values()) {
      String currentValue = myComponent.getAttributeValueString(p.getName());
      if (currentValue != null && currentValue.length() > 0) {
        String translation = getTranslatedValue(p.getName(), t);
        if (translation == null || translation.length() == 0) {
          return true;
        }
      }
    }
    /*
     * Check Children
     */
    for (Translatable child : children) {
      if (child.getI18nData().hasUntranslatedAttributes(t)) {
        return true;
      }
    }
    /*
     * Nothing left untranslated!
     */
    return false;
  }
  /** An attribute of a Configurable component that can be translated into another language */
  public static class Property {
    private String name;
    private String description;
    private String untranslatedValue;

    public Property(String name, String description) {
      super();
      this.name = name;
      this.description = description;
      this.untranslatedValue = name;
    }

    public String getDescription() {
      return description;
    }

    public String getName() {
      return name;
    }

    public String getUntranslatedValue() {
      return untranslatedValue;
    }

    public void setUntranslatedValue(String untranslatedValue) {
      this.untranslatedValue = untranslatedValue;
    }
  }
}
