/*
 * CatalogTexture.java 5 oct. 07
 *
 * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * A texture in textures catalog.
 * @author Emmanuel Puybaret
 */
public class CatalogTexture implements TextureImage, CatalogItem, Comparable<CatalogTexture> {
  private static final long serialVersionUID = 1L;
  private static final byte [][]  EMPTY_CRITERIA     = new byte [0][];
 
  private final String          id;
  private final String          name;
  private final Content         image;
  private final float           width;
  private final float           height;
  private final String          creator;
  private final boolean         modifiable;
  
  private TexturesCategory      category;
  private byte []               filterCollationKey;
  
  private static final Collator COMPARATOR;
  private static final Map<String, byte [][]> recentFilters;
  
  static {
    COMPARATOR = Collator.getInstance();
    COMPARATOR.setStrength(Collator.PRIMARY); 
    recentFilters = new WeakHashMap<String, byte[][]>();
  }

  /**
   * Creates an unmodifiable catalog texture.
   * @param name the name of this texture 
   * @param image the content of the image used for this texture
   * @param width the width of the texture in centimeters
   * @param height the height of the texture in centimeters
   */
  public CatalogTexture(String name, Content image, float width, float height) {
    this(null, name, image, width, height, null);
  }

  /**
   * Creates a catalog texture.
   * @param id   the id of the texture
   * @param name the name of this texture 
   * @param image the content of the image used for this texture
   * @param width the width of the texture in centimeters
   * @param height the height of the texture in centimeters
   * @param creator the creator of this texture
   */
  public CatalogTexture(String id, 
                        String name, Content image, 
                        float width, float height,
                        String creator) {
    this(id, name, image, width, height, creator, false);
  }
  
  /**
   * Creates a catalog texture.
   * @param name the name of this texture 
   * @param image the content of the image used for this texture
   * @param width the width of the texture in centimeters
   * @param height the height of the texture in centimeters
   * @param modifiable <code>true</code> if this texture can be modified
   */
  public CatalogTexture(String name, Content image, 
                        float width, float height,
                        boolean modifiable) {
    this(null, name, image, width, height, null, modifiable);
  }
  
  /**
   * Creates a catalog texture.
   * @param id the ID of this texture 
   * @param name the name of this texture 
   * @param image the content of the image used for this texture
   * @param width the width of the texture in centimeters
   * @param height the height of the texture in centimeters
   * @param modifiable <code>true</code> if this texture can be modified
   */
  public CatalogTexture(String id, 
                        String name, Content image, 
                        float width, float height,
                        String creator,
                        boolean modifiable) {
    this.id = id;
    this.name = name;
    this.image = image;
    this.width = width;
    this.height = height;
    this.creator = creator;
    this.modifiable = modifiable;
  }
  
  /**
   * Returns the ID of this texture or <code>null</code>.
   * @since 2.3
   */
  public String getId() {
    return this.id;
  }

  /**
   * Returns the name of this texture.
   */
  public String getName() {
    return this.name;
  }
  
  /**
   * Returns the content of the image used for this texture. 
   */
  public Content getImage() {
    return this.image;
  }
  
  /**
   * Returns the icon of this texture.
   * @return the image of this texture.
   * @since 4.4
   */
  public Content getIcon() {
    return getImage();
  }
  
  /**
   * Returns the width of the image in centimeters.
   */
  public float getWidth() {
    return this.width;
  }

  /**
   * Returns the height of the image in centimeters.
   */
  public float getHeight() {
    return this.height;
  }
  
  /**
   * Returns the creator of this texture or <code>null</code>.
   * @since 2.3
   */
  public String getCreator() {
    return this.creator;
  }

  /**
   * Returns <code>true</code> if this texture is modifiable (not read from resources).
   */
  public boolean isModifiable() {
    return this.modifiable;
  }
  
  /**
   * Returns the category of this texture.
   */
  public TexturesCategory getCategory() {
    return this.category;
  }
  
  /**
   * Sets the category of this texture.
   */
  void setCategory(TexturesCategory category) {
    this.category = category;
  }
  
  /** 
   * Returns true if this texture and the one in parameter are the same objects.
   * Note that, from version 3.6, two textures can have the same name.
   */
  @Override
  public boolean equals(Object obj) {
    return super.equals(obj);
  }

  /** 
   * Returns default hash code.
   */
   @Override
  public int hashCode() {
    return super.hashCode();
  }

  /** 
   * Compares the names of this texture and the one in parameter.
   */
  public int compareTo(CatalogTexture texture) {
    int nameComparison = COMPARATOR.compare(this.name, texture.name);
    if (nameComparison != 0) {
      return nameComparison;
    } else {
      return this.modifiable == texture.modifiable 
          ? 0
          : (this.modifiable ? 1 : -1); 
    }
  }

  /**
   * Returns <code>true</code> if this texture matches the given <code>filter</code> text. 
   * Each substring of the <code>filter</code> is considered as a search criterion that can match
   * the name, the category name or the creator of this texture.
   * @since 4.4  
   */
  public boolean matchesFilter(String filter) {
    byte [][] filterCriteriaCollationKeys = getFilterCollationKeys(filter);
    int checkedCriteria = 0;
    if (filterCriteriaCollationKeys.length > 0) {
      byte [] furnitureCollationKey = getTextureCollationKey();
      for (int i = 0; i < filterCriteriaCollationKeys.length; i++) {
        if (isSubCollationKey(furnitureCollationKey, filterCriteriaCollationKeys [i], 0)) {
          checkedCriteria++;
        } else {
          break;
        }
      }
    }
    return checkedCriteria == filterCriteriaCollationKeys.length;
  }
  
  /**
   * Returns the collation key bytes of each criterion in the given <code>filter</code>.
   */
  private byte [][] getFilterCollationKeys(String filter) {
    if (filter.length() == 0) {
      return EMPTY_CRITERIA;
    }
    byte [][] filterCollationKeys = recentFilters.get(filter);
    if (filterCollationKeys == null) {
      // Each substring in filter is a search criterion that must be verified 
      String [] filterCriteria = filter.split("\\s|\\p{Punct}|\\|");
      List<byte []> filterCriteriaCollationKeys = new ArrayList<byte []>(filterCriteria.length);
      for (String criterion : filterCriteria) {
        if (criterion.length() > 0) {
          filterCriteriaCollationKeys.add(COMPARATOR.getCollationKey(criterion).toByteArray());
        }
      }
      if (filterCriteriaCollationKeys.size() == 0) {
        filterCollationKeys = EMPTY_CRITERIA;
      } else {
        filterCollationKeys = filterCriteriaCollationKeys.toArray(new byte [filterCriteriaCollationKeys.size()][]);
      }
      recentFilters.put(filter, filterCollationKeys);
    }
    return filterCollationKeys;
  }

  /**
   * Returns the collation key bytes used to compare the given <code>texture</code> with filter.
   */
  private byte [] getTextureCollationKey() {
    if (this.filterCollationKey == null) {
      // Prepare filter string collation key  
      // (collect the name, category and creator of each texture)
      StringBuilder search = new StringBuilder();
      search.append(getName());
      search.append('|');
      if (getCategory() != null) {
        search.append(getCategory().getName());
        search.append('|');
      }
      if (getCreator() != null) {
        search.append(getCreator());
        search.append('|');
      }
      this.filterCollationKey = COMPARATOR.getCollationKey(search.toString()).toByteArray();
    }
    return this.filterCollationKey;
  }
  
  /**
   * Returns <code>true</code> if the given filter collation key is a sub part of the first array collator key.
   */
  private boolean isSubCollationKey(byte [] collationKey, byte [] filterCollationKey, int start) {
    // Ignore the last 4 bytes of the collator key
    for (int i = start, n = collationKey.length - 4, m = filterCollationKey.length - 4; i < n && i < n - m + 1; i++) {
      if (collationKey [i] == filterCollationKey [0]) {
        for (int j = 1; j < m; j++) {
          if (collationKey [i + j] != filterCollationKey [j]) {
            return isSubCollationKey(collationKey, filterCollationKey, i + 1);
          }
        }
        return true;
      }
    }
    return false;
  }
}
