// Filename: eggMaterialCollection.cxx
// Created by:  drose (30Apr01)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) 2001 - 2004, Disney Enterprises, Inc.  All rights reserved
//
// All use of this software is subject to the terms of the Panda 3d
// Software license.  You should have received a copy of this license
// along with this source code; you will also find a current copy of
// the license at http://etc.cmu.edu/panda3d/docs/license/ .
//
// To contact the maintainers of this program write to
// panda3d-general@lists.sourceforge.net .
//
////////////////////////////////////////////////////////////////////

#include "eggMaterialCollection.h"
#include "eggGroupNode.h"
#include "eggPrimitive.h"
#include "eggMaterial.h"

#include "nameUniquifier.h"
#include "dcast.h"

#include <algorithm>

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::Constructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
EggMaterialCollection::
EggMaterialCollection() {
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::Copy Constructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
EggMaterialCollection::
EggMaterialCollection(const EggMaterialCollection &copy) :
  _materials(copy._materials),
  _ordered_materials(copy._ordered_materials)
{
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::Copy Assignment Operator
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
EggMaterialCollection &EggMaterialCollection::
operator = (const EggMaterialCollection &copy) {
  _materials = copy._materials;
  _ordered_materials = copy._ordered_materials;
  return *this;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::Destructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
EggMaterialCollection::
~EggMaterialCollection() {
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::clear
//       Access: Public
//  Description: Removes all materials from the collection.
////////////////////////////////////////////////////////////////////
void EggMaterialCollection::
clear() {
  _materials.clear();
  _ordered_materials.clear();
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::extract_materials
//       Access: Public
//  Description: Walks the egg hierarchy beginning at the indicated
//               node, and removes any EggMaterials encountered in the
//               hierarchy, adding them to the collection.  Returns
//               the number of EggMaterials encountered.
////////////////////////////////////////////////////////////////////
int EggMaterialCollection::
extract_materials(EggGroupNode *node) {
  // Since this traversal is destructive, we'll handle it within the
  // EggGroupNode code.
  return node->find_materials(this);
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::insert_materials
//       Access: Public
//  Description: Adds a series of EggMaterial nodes to the beginning of
//               the indicated node to reflect each of the materials in
//               the collection.  Returns an iterator representing the
//               first position after the newly inserted materials.
////////////////////////////////////////////////////////////////////
EggGroupNode::iterator EggMaterialCollection::
insert_materials(EggGroupNode *node) {
  return insert_materials(node, node->begin());
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::insert_materials
//       Access: Public
//  Description: Adds a series of EggMaterial nodes to the beginning of
//               the indicated node to reflect each of the materials in
//               the collection.  Returns an iterator representing the
//               first position after the newly inserted materials.
////////////////////////////////////////////////////////////////////
EggGroupNode::iterator EggMaterialCollection::
insert_materials(EggGroupNode *node, EggGroupNode::iterator position) {
  OrderedMaterials::iterator oti;
  for (oti = _ordered_materials.begin();
       oti != _ordered_materials.end();
       ++oti) {
    EggMaterial *material = (*oti);
    position = node->insert(position, material);
  }

  return position;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::find_used_materials
//       Access: Public
//  Description: Walks the egg hierarchy beginning at the indicated
//               node, looking for materials that are referenced by
//               primitives but are not already members of the
//               collection, adding them to the collection.
//
//               If this is called following extract_materials(), it
//               can be used to pick up any additional material
//               references that appeared in the egg hierarchy (but
//               whose EggMaterial node was not actually part of the
//               hierarchy).
//
//               If this is called in lieu of extract_materials(), it
//               will fill up the collection with all of the
//               referenced materials (and only the referenced
//               materials), without destructively removing the
//               EggMaterials from the hierarchy.
//
//               This also has the side effect of incrementing the
//               internal usage count for a material in the collection
//               each time a material reference is encountered.  This
//               side effect is taken advantage of by
//               remove_unused_materials().
////////////////////////////////////////////////////////////////////
int EggMaterialCollection::
find_used_materials(EggNode *node) {
  int num_found = 0;

  if (node->is_of_type(EggPrimitive::get_class_type())) {
    EggPrimitive *primitive = DCAST(EggPrimitive, node);
    if (primitive->has_material()) {
      EggMaterial *tex = primitive->get_material();
      Materials::iterator ti = _materials.find(tex);
      if (ti == _materials.end()) {
        // Here's a new material!
        num_found++;
        _materials.insert(Materials::value_type(tex, 1));
        _ordered_materials.push_back(tex);
      } else {
        // Here's a material we'd already known about.  Increment its
        // usage count.
        (*ti).second++;
      }
    }

  } else if (node->is_of_type(EggGroupNode::get_class_type())) {
    EggGroupNode *group = DCAST(EggGroupNode, node);

    EggGroupNode::iterator ci;
    for (ci = group->begin(); ci != group->end(); ++ci) {
      EggNode *child = *ci;

      num_found += find_used_materials(child);
    }
  }

  return num_found;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::remove_unused_materials
//       Access: Public
//  Description: Removes any materials from the collection that aren't
//               referenced by any primitives in the indicated egg
//               hierarchy.  This also, incidentally, adds materials to
//               the collection that had been referenced by primitives
//               but had not previously appeared in the collection.
////////////////////////////////////////////////////////////////////
void EggMaterialCollection::
remove_unused_materials(EggNode *node) {
  // We'll do this the easy way: First, we'll remove *all* the
  // materials from the collection, and then we'll add back only those
  // that appear in the hierarchy.
  clear();
  find_used_materials(node);
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::collapse_equivalent_materials
//       Access: Public
//  Description: Walks through the collection and collapses together
//               any separate materials that are equivalent according
//               to the indicated equivalence factor, eq (see
//               EggMaterial::is_equivalent_to()).  The return value is
//               the number of materials removed.
//
//               This flavor of collapse_equivalent_materials()
//               automatically adjusts all the primitives in the egg
//               hierarchy to refer to the new material pointers.
////////////////////////////////////////////////////////////////////
int EggMaterialCollection::
collapse_equivalent_materials(int eq, EggGroupNode *node) {
  MaterialReplacement removed;
  int num_collapsed = collapse_equivalent_materials(eq, removed);

  // And now walk the egg hierarchy and replace any references to a
  // removed material with its replacement.
  replace_materials(node, removed);

  return num_collapsed;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::collapse_equivalent_materials
//       Access: Public
//  Description: Walks through the collection and collapses together
//               any separate materials that are equivalent according
//               to the indicated equivalence factor, eq (see
//               EggMaterial::is_equivalent_to()).  The return value is
//               the number of materials removed.
//
//               This flavor of collapse_equivalent_materials() does
//               not adjust any primitives in the egg hierarchy;
//               instead, it fills up the 'removed' map with an entry
//               for each removed material, mapping it back to the
//               equivalent retained material.  It's up to the user to
//               then call replace_materials() with this map, if
//               desired, to apply these changes to the egg hierarchy.
////////////////////////////////////////////////////////////////////
int EggMaterialCollection::
collapse_equivalent_materials(int eq, EggMaterialCollection::MaterialReplacement &removed) {
  int num_collapsed = 0;

  typedef pset<PT(EggMaterial), UniqueEggMaterials> Collapser;
  UniqueEggMaterials uet(eq);
  Collapser collapser(uet);

  // First, put all of the materials into the Collapser structure, to
  // find out the unique materials.
  OrderedMaterials::const_iterator oti;
  for (oti = _ordered_materials.begin();
       oti != _ordered_materials.end();
       ++oti) {
    EggMaterial *tex = (*oti);

    pair<Collapser::const_iterator, bool> result = collapser.insert(tex);
    if (!result.second) {
      // This material is non-unique; another one was already there.
      EggMaterial *first = *(result.first);
      removed.insert(MaterialReplacement::value_type(tex, first));
      num_collapsed++;
    }
  }

  // Now record all of the unique materials only.
  clear();
  Collapser::const_iterator ci;
  for (ci = collapser.begin(); ci != collapser.end(); ++ci) {
    add_material(*ci);
  }

  return num_collapsed;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::replace_materials
//       Access: Public, Static
//  Description: Walks the egg hierarchy, changing out any reference
//               to a material appearing on the left side of the map
//               with its corresponding material on the right side.
//               This is most often done following a call to
//               collapse_equivalent_materials().  It does not directly
//               affect the Collection.
////////////////////////////////////////////////////////////////////
void EggMaterialCollection::
replace_materials(EggGroupNode *node,
                 const EggMaterialCollection::MaterialReplacement &replace) {
  EggGroupNode::iterator ci;
  for (ci = node->begin();
       ci != node->end();
       ++ci) {
    EggNode *child = *ci;
    if (child->is_of_type(EggPrimitive::get_class_type())) {
      EggPrimitive *primitive = DCAST(EggPrimitive, child);
      if (primitive->has_material()) {
        PT(EggMaterial) tex = primitive->get_material();
        MaterialReplacement::const_iterator ri;
        ri = replace.find(tex);
        if (ri != replace.end()) {
          // Here's a material we want to replace.
          primitive->set_material((*ri).second);
        }
      }

    } else if (child->is_of_type(EggGroupNode::get_class_type())) {
      EggGroupNode *group_child = DCAST(EggGroupNode, child);
      replace_materials(group_child, replace);
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::uniquify_mrefs
//       Access: Public
//  Description: Guarantees that each material in the collection has a
//               unique MRef name.  This is essential before writing
//               an egg file.
////////////////////////////////////////////////////////////////////
void EggMaterialCollection::
uniquify_mrefs() {
  NameUniquifier nu(".mref", "mref");

  OrderedMaterials::const_iterator oti;
  for (oti = _ordered_materials.begin();
       oti != _ordered_materials.end();
       ++oti) {
    EggMaterial *tex = (*oti);

    tex->set_name(nu.add_name(tex->get_name()));
  }
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::sort_by_mref
//       Access: Public
//  Description: Sorts all the materials into alphabetical order by
//               MRef name.  Subsequent operations using begin()/end()
//               will traverse in this sorted order.
////////////////////////////////////////////////////////////////////
void EggMaterialCollection::
sort_by_mref() {
  sort(_ordered_materials.begin(), _ordered_materials.end(),
       NamableOrderByName());
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::add_material
//       Access: Public
//  Description: Explicitly adds a new material to the collection.
//               Returns true if the material was added, false if it
//               was already there or if there was some error.
////////////////////////////////////////////////////////////////////
bool EggMaterialCollection::
add_material(EggMaterial *material) {
  nassertr(_materials.size() == _ordered_materials.size(), false);

  PT(EggMaterial) new_tex = material;

  Materials::const_iterator ti;
  ti = _materials.find(new_tex);
  if (ti != _materials.end()) {
    // This material is already a member of the collection.
    return false;
  }

  _materials.insert(Materials::value_type(new_tex, 0));
  _ordered_materials.push_back(new_tex);

  nassertr(_materials.size() == _ordered_materials.size(), false);
  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::remove_material
//       Access: Public
//  Description: Explicitly removes a material from the collection.
//               Returns true if the material was removed, false if it
//               wasn't there or if there was some error.
////////////////////////////////////////////////////////////////////
bool EggMaterialCollection::
remove_material(EggMaterial *material) {
  nassertr(_materials.size() == _ordered_materials.size(), false);

  Materials::iterator ti;
  ti = _materials.find(material);
  if (ti == _materials.end()) {
    // This material is not a member of the collection.
    return false;
  }

  _materials.erase(ti);

  OrderedMaterials::iterator oti;
  PT(EggMaterial) ptex = material;
  oti = find(_ordered_materials.begin(), _ordered_materials.end(), ptex);
  nassertr(oti != _ordered_materials.end(), false);

  _ordered_materials.erase(oti);

  nassertr(_materials.size() == _ordered_materials.size(), false);
  return true;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::create_unique_material
//       Access: Public
//  Description: Creates a new material if there is not already one
//               equivalent (according to eq, see
//               EggMaterial::is_equivalent_to()) to the indicated
//               material, or returns the existing one if there is.
////////////////////////////////////////////////////////////////////
EggMaterial *EggMaterialCollection::
create_unique_material(const EggMaterial &copy, int eq) {
  // This requires a complete linear traversal, not terribly
  // efficient.
  OrderedMaterials::const_iterator oti;
  for (oti = _ordered_materials.begin();
       oti != _ordered_materials.end();
       ++oti) {
    EggMaterial *tex = (*oti);
    if (copy.is_equivalent_to(*tex, eq)) {
      return tex;
    }
  }

  EggMaterial *new_material = new EggMaterial(copy);
  add_material(new_material);
  return new_material;
}

////////////////////////////////////////////////////////////////////
//     Function: EggMaterialCollection::find_mref
//       Access: Public
//  Description: Returns the material with the indicated MRef name, or
//               NULL if no material matches.
////////////////////////////////////////////////////////////////////
EggMaterial *EggMaterialCollection::
find_mref(const string &mref_name) const {
  // This requires a complete linear traversal, not terribly
  // efficient.
  OrderedMaterials::const_iterator oti;
  for (oti = _ordered_materials.begin();
       oti != _ordered_materials.end();
       ++oti) {
    EggMaterial *tex = (*oti);
    if (tex->get_name() == mref_name) {
      return tex;
    }
  }

  return (EggMaterial *)NULL;
}
