// Filename: textureImage.cxx
// Created by:  drose (29Nov00)
//
////////////////////////////////////////////////////////////////////
//
// 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 "textureImage.h"
#include "sourceTextureImage.h"
#include "destTextureImage.h"
#include "eggFile.h"
#include "paletteGroup.h"
#include "paletteImage.h"
#include "texturePlacement.h"
#include "filenameUnifier.h"

#include "indent.h"
#include "datagram.h"
#include "datagramIterator.h"
#include "bamReader.h"
#include "bamWriter.h"
#include "pnmFileType.h"
#include "indirectCompareNames.h"
#include "pvector.h"

TypeHandle TextureImage::_type_handle;

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::Constructor
//       Access: Public
//  Description:
////////////////////////////////////////////////////////////////////
TextureImage::
TextureImage() {
  _preferred_source = (SourceTextureImage *)NULL;
  _read_source_image = false;
  _is_surprise = true;
  _ever_read_image = false;
  _forced_grayscale = false;
  _alpha_bits = 0;
  _alpha_mode = EggRenderMode::AM_unspecified;
  _texture_named = false;
  _got_txa_file = false;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::note_egg_file
//       Access: Public
//  Description: Records that a particular egg file references this
//               texture.  This is essential to know when deciding how
//               to assign the TextureImage to the various
//               PaletteGroups.
////////////////////////////////////////////////////////////////////
void TextureImage::
note_egg_file(EggFile *egg_file) {
  nassertv(!egg_file->get_complete_groups().empty());
  _egg_files.insert(egg_file);
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::assign_groups
//       Access: Public
//  Description: Assigns the texture to all of the PaletteGroups the
//               various egg files that use it need.  Attempts to
//               choose the minimum set of PaletteGroups that
//               satisfies all of the egg files.
////////////////////////////////////////////////////////////////////
void TextureImage::
assign_groups() {
  if (_egg_files.empty()) {
    // If we're not referenced by any egg files any more, assign us to
    // no groups.
    PaletteGroups empty;
    assign_to_groups(empty);
    return;
  }

  PaletteGroups definitely_in;

  // First, we need to eliminate from consideration all the egg files
  // that are already taken care of by the user's explicit group
  // assignments for this texture.
  WorkingEggs needed_eggs;

  if (_explicitly_assigned_groups.empty()) {
    // If we have no explicit group assignments, we must consider all
    // the egg files.
    copy(_egg_files.begin(), _egg_files.end(), back_inserter(needed_eggs));

  } else {
    // Otherwise, we only need to consider the egg files that don't
    // have any groups in common with our explicit assignments.

    EggFiles::const_iterator ei;
    for (ei = _egg_files.begin(); ei != _egg_files.end(); ++ei) {
      PaletteGroups intersect;
      intersect.make_intersection(_explicitly_assigned_groups, (*ei)->get_complete_groups());
      if (!intersect.empty()) {
        // This egg file is satisfied by one of the texture's explicit
        // assignments.

        // We must use at least one of the explicitly-assigned groups
        // that satisfied the egg file.  We don't need to use all of
        // them, however, and we choose the first one arbitrarily.
        definitely_in.insert(*intersect.begin());

      } else {
        // This egg file was not satisfied by any of the texture's
        // explicit assignments.  Therefore, we'll need to choose some
        // additional group to assign the texture to, to make the egg
        // file happy.  Defer this a bit.
        needed_eggs.push_back(*ei);
      }
    }
  }

  while (!needed_eggs.empty()) {
    // We need to know the complete set of groups that we need to
    // consider adding the texture to.  This is the union of all the egg
    // files' requested groups.
    PaletteGroups total;
    WorkingEggs::const_iterator ei;
    for (ei = needed_eggs.begin(); ei != needed_eggs.end(); ++ei) {
      total.make_union(total, (*ei)->get_complete_groups());
    }

    // Now, find the group that will satisfy the most egg files.  If
    // two groups satisfy the same number of egg files, choose (a) the
    // most specific one, i.e. with the lowest dirname_level, or the
    // lowest dependency_level if the dirname_levels are equal, and
    // (b) the one that has the fewest egg files sharing it.
    nassertv(!total.empty());
    PaletteGroups::iterator gi = total.begin();
    PaletteGroup *best = (*gi);
    int best_egg_count = compute_egg_count(best, needed_eggs);
    ++gi;
    while (gi != total.end()) {
      PaletteGroup *group = (*gi);

      // Do we prefer this group to our current 'best'?
      bool prefer_group = false;
      int group_egg_count = compute_egg_count(group, needed_eggs);
      if (group_egg_count != best_egg_count) {
        prefer_group = (group_egg_count > best_egg_count);

      } else {
        prefer_group = group->is_preferred_over(*best);
      }

      if (prefer_group) {
        best = group;
        best_egg_count = group_egg_count;
      }
      ++gi;
    }

    // Okay, now we've picked the best group.  Eliminate all the eggs
    // from consideration that are satisfied by this group, and repeat.
    definitely_in.insert(best);

    WorkingEggs next_needed_eggs;
    for (ei = needed_eggs.begin(); ei != needed_eggs.end(); ++ei) {
      if ((*ei)->get_complete_groups().count(best) == 0) {
        // This one wasn't eliminated.
        next_needed_eggs.push_back(*ei);
      }
    }
    needed_eggs.swap(next_needed_eggs);
  }

  // Finally, now that we've computed the set of groups we need to
  // assign the texture to, we need to reconcile this with the set of
  // groups we've assigned the texture to previously.
  assign_to_groups(definitely_in);
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_groups
//       Access: Public
//  Description: Once assign_groups() has been called, this returns
//               the actual set of groups the TextureImage has been
//               assigned to.
////////////////////////////////////////////////////////////////////
const PaletteGroups &TextureImage::
get_groups() const {
  return _actual_assigned_groups;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_placement
//       Access: Public
//  Description: Gets the TexturePlacement object which represents the
//               assignment of this texture to the indicated group.
//               If the texture has not been assigned to the indicated
//               group, returns NULL.
////////////////////////////////////////////////////////////////////
TexturePlacement *TextureImage::
get_placement(PaletteGroup *group) const {
  Placement::const_iterator pi;
  pi = _placement.find(group);
  if (pi == _placement.end()) {
    return (TexturePlacement *)NULL;
  }

  return (*pi).second;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::force_replace
//       Access: Public
//  Description: Removes the texture from any PaletteImages it is
//               assigned to, but does not remove it from the groups.
//               It will be re-placed within each group when
//               PaletteGroup::place_all() is called.
////////////////////////////////////////////////////////////////////
void TextureImage::
force_replace() {
  Placement::iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    (*pi).second->force_replace();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::mark_eggs_stale
//       Access: Public
//  Description: Marks all the egg files that reference this texture
//               stale.  Should be called only when the texture
//               properties change in some catastrophic way that will
//               require every egg file referencing it to be
//               regenerated, even if it is not palettized.
////////////////////////////////////////////////////////////////////
void TextureImage::
mark_eggs_stale() {
  Placement::iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    (*pi).second->mark_eggs_stale();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::mark_texture_named
//       Access: Public
//  Description: Indicates that this particular texture has been named
//               by the user for processing this session, normally by
//               listing an egg file on the command line that
//               references it.
////////////////////////////////////////////////////////////////////
void TextureImage::
mark_texture_named() {
  _texture_named = true;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::is_texture_named
//       Access: Public
//  Description: Returns true if this particular texture has been
//               named by the user for procession this session, for
//               instance by listing an egg file on the command line
//               that references it.
////////////////////////////////////////////////////////////////////
bool TextureImage::
is_texture_named() const {
  return _texture_named;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::pre_txa_file
//       Access: Public
//  Description: Updates any internal state prior to reading the .txa
//               file.
////////////////////////////////////////////////////////////////////
void TextureImage::
pre_txa_file() {
  // Save our current properties, so we can note if they change.
  _pre_txa_properties = _properties;

  // Get our properties from the actual image for this texture.  It's
  // possible the .txa file will update them further.
  SourceTextureImage *source = get_preferred_source();
  if (source != (SourceTextureImage *)NULL) {
    _properties = source->get_properties();
  }

  _pre_txa_alpha_mode = _alpha_mode;
  _alpha_mode = EggRenderMode::AM_unspecified;

  _request.pre_txa_file();
  _is_surprise = true;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::post_txa_file
//       Access: Public
//  Description: Once the .txa file has been read and the TextureImage
//               matched against it, considers applying the requested
//               size change.  Updates the TextureImage's size with
//               the size the texture ought to be, if this can be
//               determined.
////////////////////////////////////////////////////////////////////
void TextureImage::
post_txa_file() {
  _got_txa_file = true;

  // First, get the actual size of the texture.
  SourceTextureImage *source = get_preferred_source();
  if (source != (SourceTextureImage *)NULL) {
    if (source->get_size()) {
      _size_known = true;
      _x_size = source->get_x_size();
      _y_size = source->get_y_size();
      _properties.set_num_channels(source->get_num_channels());
    }
  }

  // Now update this with a particularly requested size.
  if (_request._got_size) {
    _size_known = true;
    _x_size = _request._x_size;
    _y_size = _request._y_size;
  }

  if (_properties.has_num_channels() && !_request._keep_format) {
    int num_channels = _properties.get_num_channels();
    // Examine the image to determine if we can downgrade the number
    // of channels, for instance from color to grayscale.
    if (num_channels == 3 || num_channels == 4) {
      consider_grayscale();
    }
    
    // Also consider the alpha properties, and whether we should
    // downgrade from alpha to non-alpha.
    if (num_channels == 2 || num_channels == 4) {
      consider_alpha();
    }
  }

  // However, if we got an explicit request for channels, honor that.
  if (_request._got_num_channels) {
    _properties.set_num_channels(_request._num_channels);
  }

  _properties._generic_format = _request._generic_format;
  _properties._keep_format = _request._keep_format;

  if (_request._format != EggTexture::F_unspecified) {
    _properties._format = _request._format;
    _properties._force_format = _request._force_format;
  }

  if (_request._minfilter != EggTexture::FT_unspecified) {
    _properties._minfilter = _request._minfilter;
  }
  if (_request._magfilter != EggTexture::FT_unspecified) {
    _properties._magfilter = _request._magfilter;
  }

  _properties._anisotropic_degree = _request._anisotropic_degree;

  if (_properties._color_type == (PNMFileType *)NULL) {
    _properties._color_type = _request._properties._color_type;
    _properties._alpha_type = _request._properties._alpha_type;
  }

  // Finally, make sure our properties are fully defined.
  _properties.fully_define();

  // Now, if our properties have changed in all that from our previous
  // session, we need to re-place ourself in all palette groups.
  if (_properties != _pre_txa_properties) {
    force_replace();

    // The above will mark the egg files stale when the texture is
    // palettized (since the UV's will certainly need to be
    // recomputed), but sometimes we need to mark the egg files stale
    // even when the texture is not palettized (if a critical property
    // has changed).  The following accomplishes this:
    if (!_properties.egg_properties_match(_pre_txa_properties)) {
      mark_eggs_stale();
    }
  }

  // The alpha mode isn't stored in the properties, because it doesn't
  // affect which textures may be associated into a common palette.
  if (_request._alpha_mode != EggRenderMode::AM_unspecified) {
    _alpha_mode = _request._alpha_mode;
  }

  // On the other hand, if we don't have an alpha channel, we
  // shouldn't have an alpha mode.
  if (_properties.has_num_channels()) {
    int num_channels = _properties.get_num_channels();
    if (num_channels == 1 || num_channels == 3) {
      _alpha_mode = EggRenderMode::AM_unspecified;
    }
  }

  // If we've changed the alpha mode, we should also mark the eggs
  // stale.
  if (_pre_txa_alpha_mode != _alpha_mode) {
    mark_eggs_stale();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::got_txa_file
//       Access: Public
//  Description: Returns true if this TextureImage has been looked up
//               in the .txa file this session, false otherwise.
////////////////////////////////////////////////////////////////////
bool TextureImage::
got_txa_file() const {
  return _got_txa_file;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::determine_placement_size
//       Access: Public
//  Description: Calls determine_size() on each TexturePlacement for
//               the texture, to ensure that each TexturePlacement is
//               still requesting the best possible size for the
//               texture.
////////////////////////////////////////////////////////////////////
void TextureImage::
determine_placement_size() {
  Placement::iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    TexturePlacement *placement = (*pi).second;
    placement->determine_size();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_omit
//       Access: Public
//  Description: Returns true if the user specifically requested to
//               omit this texture via the "omit" keyword in the .txa
//               file, or false otherwise.
////////////////////////////////////////////////////////////////////
bool TextureImage::
get_omit() const {
  return _request._omit;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_coverage_threshold
//       Access: Public
//  Description: Returns the appropriate coverage threshold for this
//               texture.  This is either the
//               Palettizer::_coverage_threshold parameter, given
//               globally via -r, or a particular value for this
//               texture as supplied by the "coverage" keyword in the
//               .txa file.
////////////////////////////////////////////////////////////////////
double TextureImage::
get_coverage_threshold() const {
  return _request._coverage_threshold;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_margin
//       Access: Public
//  Description: Returns the appropriate margin for this texture.
//               This is either the Palettizer::_margin parameter, or
//               a particular value for this texture as supplied by
//               the "margin" keyword in the .txa file.
////////////////////////////////////////////////////////////////////
int TextureImage::
get_margin() const {
  return _request._margin;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::is_surprise
//       Access: Public
//  Description: Returns true if this particular texture is a
//               'surprise', i.e. it wasn't matched by a line in the
//               .txa file that didn't include the keyword 'cont'.
////////////////////////////////////////////////////////////////////
bool TextureImage::
is_surprise() const {
  if (_placement.empty()) {
    // A texture that is not actually placed anywhere is not
    // considered a surprise.
    return false;
  }

  return _is_surprise;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::is_used
//       Access: Public
//  Description: Returns true if this particular texture has been
//               placed somewhere, anywhere, or false if it is not
//               used.
////////////////////////////////////////////////////////////////////
bool TextureImage::
is_used() const {
  return !_placement.empty();
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_alpha_mode
//       Access: Public
//  Description: Returns the alpha mode that should be used to render
//               objects with this texture, as specified by the user
//               or as determined from examining the texture's alpha
//               channel.
////////////////////////////////////////////////////////////////////
EggRenderMode::AlphaMode TextureImage::
get_alpha_mode() const {
  return _alpha_mode;
}


////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_source
//       Access: Public
//  Description: Returns the SourceTextureImage corresponding to the
//               given filename(s).  If the given filename has never
//               been used as a SourceTexture for this particular
//               texture, creates a new SourceTextureImage and returns
//               that.
////////////////////////////////////////////////////////////////////
SourceTextureImage *TextureImage::
get_source(const Filename &filename, const Filename &alpha_filename,
           int alpha_file_channel) {
  string key = get_source_key(filename, alpha_filename, alpha_file_channel);

  Sources::iterator si;
  si = _sources.find(key);
  if (si != _sources.end()) {
    return (*si).second;
  }

  SourceTextureImage *source =
    new SourceTextureImage(this, filename, alpha_filename, alpha_file_channel);
  _sources.insert(Sources::value_type(key, source));

  // Clear out the preferred source image to force us to rederive this
  // next time someone asks.
  _preferred_source = (SourceTextureImage *)NULL;
  _read_source_image = false;

  return source;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_preferred_source
//       Access: Public
//  Description: Determines the preferred source image for examining
//               size and reading pixels, etc.  This is the largest
//               and most recent of all the available source images.
////////////////////////////////////////////////////////////////////
SourceTextureImage *TextureImage::
get_preferred_source() {
  if (_preferred_source != (SourceTextureImage *)NULL) {
    return _preferred_source;
  }

  // Now examine all of the various source images available to us and
  // pick the most suitable.  We base this on the following criteria:

  // (1) A suitable source image must be referenced by at least one
  // egg file, unless no source images are referenced by any egg file.

  // (2) A larger source image is preferable to a smaller one.

  // (3) Given two source images of the same size, the more recent one
  // is preferable.

  // Are any source images referenced by an egg file?

  bool any_referenced = false;
  Sources::iterator si;
  for (si = _sources.begin(); si != _sources.end() && !any_referenced; ++si) {
    SourceTextureImage *source = (*si).second;
    if (source->get_egg_count() > 0) {
      any_referenced = true;
    }
  }

  SourceTextureImage *best = (SourceTextureImage *)NULL;
  int best_size = 0;

  for (si = _sources.begin(); si != _sources.end(); ++si) {
    SourceTextureImage *source = (*si).second;

    if (source->get_egg_count() > 0 || !any_referenced) {
      // Rule (1) passes.

      if (source->exists() && source->get_size()) {
        int source_size = source->get_x_size() * source->get_y_size();
        if (best == (SourceTextureImage *)NULL) {
          best = source;
          best_size = source_size;

        } else if (source_size > best_size) {
          // Rule (2) passes.
          best = source;
          best_size = source_size;

        } else if (source_size == best_size &&
                   source->get_filename().compare_timestamps(best->get_filename()) > 0) {
          // Rule (3) passes.
          best = source;
          best_size = source_size;
        }
      }
    }
  }

  if (best == (SourceTextureImage *)NULL && !_sources.empty()) {
    // If we didn't pick any that pass, it must be that all of them
    // are unreadable.  In this case, it really doesn't matter which
    // one we pick, but we should at least pick one that has an egg
    // reference, if any of them do.
    if (any_referenced) {
      for (si = _sources.begin();
           si != _sources.end() && best == (SourceTextureImage *)NULL;
           ++si) {
        SourceTextureImage *source = (*si).second;
        if (source->get_egg_count() > 0) {
          best = source;
        }
      }
    } else {
      best = (*_sources.begin()).second;
    }
  }

  _preferred_source = best;
  return _preferred_source;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::clear_source_basic_properties
//       Access: Public
//  Description: Calls clear_basic_properties() on each source texture
//               image used by this texture, to reset the properties
//               in preparation for re-applying them from the set of
//               all known egg files.
////////////////////////////////////////////////////////////////////
void TextureImage::
clear_source_basic_properties() {
  Sources::iterator si;
  for (si = _sources.begin(); si != _sources.end(); ++si) {
    SourceTextureImage *source = (*si).second;
    source->clear_basic_properties();
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::copy_unplaced
//       Access: Public
//  Description: Copies the texture to whichever destination
//               directories are appropriate for the groups in which
//               it has been unplaced.  Also removes the old filenames
//               for previous sessions where it was unplaced, but is
//               no longer.
//
//               If redo_all is true, this recopies the texture
//               whether it needed to or not.
////////////////////////////////////////////////////////////////////
void TextureImage::
copy_unplaced(bool redo_all) {
  // First, we need to build up the set of DestTextureImages that
  // represents the files we need to generate.
  Dests generate;

  // Go through all the TexturePlacements and note the ones for which
  // we're unplaced.  We check get_omit_reason() and not is_placed(),
  // because we want to consider solitary images to be unplaced in
  // this case.
  Placement::iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    TexturePlacement *placement = (*pi).second;
    if (placement->get_omit_reason() != OR_none &&
        placement->get_omit_reason() != OR_unknown) {
      DestTextureImage *dest = new DestTextureImage(placement);
      Filename filename = dest->get_filename();
      FilenameUnifier::make_canonical(filename);

      pair<Dests::iterator, bool> insert_result = generate.insert
        (Dests::value_type(filename, dest));
      if (!insert_result.second) {
        // At least two DestTextureImages map to the same filename, no
        // sweat.
        delete dest;
        dest = (*insert_result.first).second;
      }

      placement->set_dest(dest);

    } else {
      placement->set_dest((DestTextureImage *)NULL);
    }
  }

  if (redo_all) {
    // If we're redoing everything, we remove everything first and
    // then recopy it again.
    Dests empty;
    remove_old_dests(empty, _dests);
    copy_new_dests(generate, empty);

  } else {
    // Otherwise, we only remove and recopy the things that changed
    // between this time and last time.
    remove_old_dests(generate, _dests);
    copy_new_dests(generate, _dests);
  }

  // Clean up the old set.
  Dests::iterator di;
  for (di = _dests.begin(); di != _dests.end(); ++di) {
    delete (*di).second;
  }

  _dests.swap(generate);
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::read_source_image
//       Access: Public
//  Description: Reads in the original image, if it has not already
//               been read, and returns it.
////////////////////////////////////////////////////////////////////
const PNMImage &TextureImage::
read_source_image() {
  if (!_read_source_image) {
    SourceTextureImage *source = get_preferred_source();
    if (source != (SourceTextureImage *)NULL) {
      source->read(_source_image);
    }
    _read_source_image = true;
    _ever_read_image = true;
  }

  return _source_image;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::release_source_image
//       Access: Public
//  Description: Frees the memory that was allocated by a previous
//               call to read_source_image().  The next time
//               read_source_image() is called, it will have to read
//               the disk again.
////////////////////////////////////////////////////////////////////
void TextureImage::
release_source_image() {
  if (_read_source_image) {
    _source_image.clear();
    _read_source_image = false;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::set_source_image
//       Access: Public
//  Description: Accepts the indicated source image as if it had been
//               read from disk.  This image is copied into the
//               structure, and will be returned by future calls to
//               read_source_image().
////////////////////////////////////////////////////////////////////
void TextureImage::
set_source_image(const PNMImage &image) {
  _source_image = image;
  _read_source_image = true;
  _ever_read_image = true;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::read_header
//       Access: Public
//  Description: Causes the header part of the image to be reread,
//               usually to confirm that its image properties (size,
//               number of channels, etc.) haven't changed.
////////////////////////////////////////////////////////////////////
void TextureImage::
read_header() {
  if (!_read_source_image) {
    SourceTextureImage *source = get_preferred_source();
    if (source != (SourceTextureImage *)NULL) {
      source->read_header();
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::is_newer_than
//       Access: Public
//  Description: Returns true if the source image is newer than the
//               indicated file, false otherwise.  If the image has
//               already been read, this always returns false.
////////////////////////////////////////////////////////////////////
bool TextureImage::
is_newer_than(const Filename &reference_filename) {
  if (!_read_source_image) {
    SourceTextureImage *source = get_preferred_source();
    if (source != (SourceTextureImage *)NULL) {
      const Filename &source_filename = source->get_filename();
      return source_filename.compare_timestamps(reference_filename) >= 0;
    }
  }

  return false;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::write_source_pathnames
//       Access: Public
//  Description: Writes the list of source pathnames that might
//               contribute to this texture to the indicated output
//               stream, one per line.
////////////////////////////////////////////////////////////////////
void TextureImage::
write_source_pathnames(ostream &out, int indent_level) const {
  Sources::const_iterator si;
  for (si = _sources.begin(); si != _sources.end(); ++si) {
    SourceTextureImage *source = (*si).second;

    if (source->get_egg_count() > 0) {
      indent(out, indent_level);
      source->output_filename(out);
      if (!source->is_size_known()) {
        out << " (unknown size)";

      } else {
        out << " " << source->get_x_size() << " "
            << source->get_y_size();

        if (source->get_properties().has_num_channels()) {
          out << " " << source->get_properties().get_num_channels();
        }
      }
      out << "\n";
    }
  }

  // Now write out the group assignments.
  if (!_egg_files.empty()) {
    // Sort the egg files into order by name for output.
    pvector<EggFile *> egg_vector;
    egg_vector.reserve(_egg_files.size());
    EggFiles::const_iterator ei;
    for (ei = _egg_files.begin(); ei != _egg_files.end(); ++ei) {
      egg_vector.push_back(*ei);
    }
    sort(egg_vector.begin(), egg_vector.end(),
         IndirectCompareNames<EggFile>());

    indent(out, indent_level)
      << "Used by:\n";
    pvector<EggFile *>::const_iterator evi;
    for (evi = egg_vector.begin(); evi != egg_vector.end(); ++evi) {
      EggFile *egg = (*evi);
      indent(out, indent_level + 2)
        << egg->get_name() << " (";
      if (egg->get_explicit_groups().empty()) {
        out << *egg->get_default_group();
      } else {
        out << egg->get_explicit_groups();
      }
      out << ")\n";
    }
  }
  if (!_explicitly_assigned_groups.empty()) {
    indent(out, indent_level)
      << "Explicitly assigned to " << _explicitly_assigned_groups << " in .txa\n";
  }

  if (_placement.empty()) {
    indent(out, indent_level)
      << "Not used.\n";
  } else {
    indent(out, indent_level)
      << "Assigned to " << _actual_assigned_groups << "\n";
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::write_scale_info
//       Access: Public
//  Description: Writes the list of source pathnames that might
//               contribute to this texture to the indicated output
//               stream, one per line.
////////////////////////////////////////////////////////////////////
void TextureImage::
write_scale_info(ostream &out, int indent_level) {
  SourceTextureImage *source = get_preferred_source();
  indent(out, indent_level) << get_name();

  // Write the list of groups we're placed in.
  if (_placement.empty()) {
    out << " (not used)";
  } else {
    Placement::const_iterator pi;
    pi = _placement.begin();
    out << " (" << (*pi).second->get_group()->get_name();
    ++pi;
    while (pi != _placement.end()) {
      out << " " << (*pi).second->get_group()->get_name();
      ++pi;
    }
    out << ")";
  }

  out << " orig ";

  if (source == (SourceTextureImage *)NULL ||
      !source->is_size_known()) {
    out << "unknown";
  } else {
    out << source->get_x_size() << " " << source->get_y_size()
        << " " << source->get_num_channels();
  }

  if (!_placement.empty() && is_size_known()) {
    out << " new " << get_x_size() << " " << get_y_size()
        << " " << get_num_channels();

    if (source != (SourceTextureImage *)NULL &&
        source->is_size_known()) {
      double scale =
        100.0 * (((double)get_x_size() / (double)source->get_x_size()) +
                 ((double)get_y_size() / (double)source->get_y_size())) / 2.0;
      out << " scale " << scale << "%";
    }
  }
  out << "\n";

  // Also cross-reference the placed and unplaced information.
  Placement::iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    TexturePlacement *placement = (*pi).second;
    if (placement->get_omit_reason() == OR_none) {
      PaletteImage *image = placement->get_image();
      nassertv(image != (PaletteImage *)NULL);
      indent(out, indent_level + 2)
        << "placed on "
        << FilenameUnifier::make_user_filename(image->get_filename())
        << "\n";

    } else if (placement->get_omit_reason() == OR_unknown) {
      indent(out, indent_level + 2)
        << "not placed because unknown.\n";

    } else {
      DestTextureImage *image = placement->get_dest();
      nassertv(image != (DestTextureImage *)NULL);
      indent(out, indent_level + 2)
        << "copied to "
        << FilenameUnifier::make_user_filename(image->get_filename());
      if (image->is_size_known() && is_size_known() &&
          (image->get_x_size() != get_x_size() ||
           image->get_y_size() != get_y_size())) {
        out << " at size " << image->get_x_size() << " "
            << image->get_y_size();
        if (source != (SourceTextureImage *)NULL &&
            source->is_size_known()) {
          double scale =
            100.0 * (((double)image->get_x_size() / (double)source->get_x_size()) +
                     ((double)image->get_y_size() / (double)source->get_y_size())) / 2.0;
          out << " scale " << scale << "%";
        }
      }
      out << "\n";
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::compute_egg_count
//       Access: Private
//  Description: Counts the number of egg files in the indicated set
//               that will be satisfied if a texture is assigned to
//               the indicated group.
////////////////////////////////////////////////////////////////////
int TextureImage::
compute_egg_count(PaletteGroup *group,
                  const TextureImage::WorkingEggs &egg_files) {
  int count = 0;

  WorkingEggs::const_iterator ei;
  for (ei = egg_files.begin(); ei != egg_files.end(); ++ei) {
    if ((*ei)->get_complete_groups().count(group) != 0) {
      count++;
    }
  }

  return count;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::assign_to_groups
//       Access: Private
//  Description: Assigns the texture to the indicated set of groups.
//               If the texture was previously assigned to any of
//               these groups, keeps the same TexturePlacement object
//               for the assignment; at the same time, deletes any
//               TexturePlacement objects that represent groups we are
//               no longer assigned to.
////////////////////////////////////////////////////////////////////
void TextureImage::
assign_to_groups(const PaletteGroups &groups) {
  PaletteGroups::const_iterator gi;
  Placement::const_iterator pi;

  Placement new_placement;

  gi = groups.begin();
  pi = _placement.begin();

  while (gi != groups.end() && pi != _placement.end()) {
    PaletteGroup *a = (*gi);
    PaletteGroup *b = (*pi).first;

    if (a < b) {
      // Here's a group we're now assigned to that we weren't assigned
      // to previously.
      TexturePlacement *place = a->prepare(this);
      new_placement.insert
        (new_placement.end(), Placement::value_type(a, place));
      ++gi;

    } else if (b < a) {
      // Here's a group we're no longer assigned to.
      TexturePlacement *place = (*pi).second;
      delete place;
      ++pi;

    } else { // b == a
      // Here's a group we're still assigned to.
      TexturePlacement *place = (*pi).second;
      new_placement.insert
        (new_placement.end(), Placement::value_type(a, place));
      ++gi;
      ++pi;
    }
  }

  while (gi != groups.end()) {
    // Here's a group we're now assigned to that we weren't assigned
    // to previously.
    PaletteGroup *a = (*gi);
    TexturePlacement *place = a->prepare(this);
    new_placement.insert
      (new_placement.end(), Placement::value_type(a, place));
    ++gi;
  }

  while (pi != _placement.end()) {
    // Here's a group we're no longer assigned to.
    TexturePlacement *place = (*pi).second;
    delete place;
    ++pi;
  }

  _placement.swap(new_placement);
  _actual_assigned_groups = groups;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::consider_grayscale
//       Access: Private
//  Description: Examines the actual contents of the image to
//               determine if it should maybe be considered a
//               grayscale image (even though it has separate rgb
//               components).
////////////////////////////////////////////////////////////////////
void TextureImage::
consider_grayscale() {
  // Since this isn't likely to change for a particular texture after
  // its creation, we save a bit of time by not performing this check
  // unless this is the first time we've ever seen this texture.  This
  // will save us from having to load the texture images each time we
  // look at them.  On the other hand, if we've already loaded up the
  // image, then go ahead.
  if (!_read_source_image && _ever_read_image) {
    if (_forced_grayscale) {
      _properties.force_grayscale();
    }
    return;
  }

  const PNMImage &source = read_source_image();
  if (!source.is_valid()) {
    return;
  }

  for (int y = 0; y < source.get_y_size(); y++) {
    for (int x = 0; x < source.get_x_size(); x++) {
      const xel &v = source.get_xel_val(x, y);
      if (PPM_GETR(v) != PPM_GETG(v) || PPM_GETR(v) != PPM_GETB(v)) {
        // Here's a colored pixel.  We can't go grayscale.
        _forced_grayscale = false;
        return;
      }
    }
  }

  // All pixels in the image were grayscale!
  _properties.force_grayscale();
  _forced_grayscale = true;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::consider_alpha
//       Access: Private
//  Description: Examines the actual contents of the image to
//               determine what alpha properties it has.
////////////////////////////////////////////////////////////////////
void TextureImage::
consider_alpha() {
  // As above, we don't bother doing this if we've already done this
  // in a previous session.

  // _alpha_bits == -1 indicates we have read an older textures.boo
  // file that didn't define these bits.
  if (_read_source_image || !_ever_read_image || _alpha_bits == -1) {
    _alpha_bits = 0;

    const PNMImage &source = read_source_image();
    if (source.is_valid() && source.has_alpha()) {
      xelval maxval = source.get_maxval();
      for (int y = 0; y < source.get_y_size(); y++) {
        for (int x = 0; x < source.get_x_size(); x++) {
          xelval alpha_val = source.get_alpha_val(x, y);
          if (alpha_val == 0) {
            _alpha_bits |= AB_zero;
          } else if (alpha_val == maxval) {
            _alpha_bits |= AB_one;
          } else {
            _alpha_bits |= AB_mid;
          }
          if (_alpha_bits == AB_all) {
            // Once we've found a sample of everything, we can stop
            // searching.
            break;
          }
        }
      }
    }
  }

  if (_alpha_bits != 0) {
    if (_alpha_bits == AB_one) {
      // All alpha pixels are white; drop the alpha channel.
      _properties.force_nonalpha();

    } else if (_alpha_bits == AB_zero) {
      // All alpha pixels are invisible; this is probably a mistake.
      // Drop the alpha channel and complain.
      _properties.force_nonalpha();
      if (_read_source_image) {
        nout << *this << " has an all-zero alpha channel; dropping alpha.\n";
      }

    } else if (_alpha_mode == EggRenderMode::AM_unspecified) {
      // Consider fiddling with the alpha mode, if the user hasn't
      // specified a particular alpha mode in the txa file.
      if ((_alpha_bits & AB_mid) == 0) {
        // No middle range bits: a binary alpha image.
        _alpha_mode = EggRenderMode::AM_binary;

      } else if ((_alpha_bits & AB_one) != 0) {
        // At least some opaque bits: a dual alpha image.
        _alpha_mode = EggRenderMode::AM_dual;

      } else {
        // No opaque bits; just use regular alpha blending.
        _alpha_mode = EggRenderMode::AM_blend;
      }
    }
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::remove_old_dests
//       Access: Private
//  Description: Removes all of the filenames named in b that are not
//               also named in a.
////////////////////////////////////////////////////////////////////
void TextureImage::
remove_old_dests(const TextureImage::Dests &a, const TextureImage::Dests &b) {
  Dests::const_iterator ai = a.begin();
  Dests::const_iterator bi = b.begin();

  while (ai != a.end() && bi != b.end()) {
    const string &astr = (*ai).first;
    const string &bstr = (*bi).first;

    if (astr < bstr) {
      // Here's a filename in a, not in b.
      ++ai;

    } else if (bstr < astr) {
      // Here's a filename in b, not in a.
      (*bi).second->unlink();
      ++bi;

    } else { // bstr == astr
      // Here's a filename in both a and b.
      ++ai;
      ++bi;
    }
  }

  while (bi != b.end()) {
    // Here's a filename in b, not in a.
    (*bi).second->unlink();
    ++bi;
  }

  while (ai != a.end()) {
    ++ai;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::copy_new_dests
//       Access: Private
//  Description: Copies a resized texture into each filename named in
//               a that is not also listed in b, or whose
//               corresponding listing in b is out of date.
////////////////////////////////////////////////////////////////////
void TextureImage::
copy_new_dests(const TextureImage::Dests &a, const TextureImage::Dests &b) {
  Dests::const_iterator ai = a.begin();
  Dests::const_iterator bi = b.begin();

  while (ai != a.end() && bi != b.end()) {
    const string &astr = (*ai).first;
    const string &bstr = (*bi).first;

    if (astr < bstr) {
      // Here's a filename in a, not in b.
      (*ai).second->copy(this);
      ++ai;

    } else if (bstr < astr) {
      // Here's a filename in b, not in a.
      ++bi;

    } else { // bstr == astr
      // Here's a filename in both a and b.
      (*ai).second->copy_if_stale((*bi).second, this);
      ++ai;
      ++bi;
    }
  }

  while (ai != a.end()) {
    // Here's a filename in a, not in b.
    (*ai).second->copy(this);
    ++ai;
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::get_source_key
//       Access: Private
//  Description: Returns the key that a SourceTextureImage should be
//               stored in, given its one or two filenames.
////////////////////////////////////////////////////////////////////
string TextureImage::
get_source_key(const Filename &filename, const Filename &alpha_filename,
               int alpha_file_channel) {
  Filename f = FilenameUnifier::make_bam_filename(filename);
  Filename a = FilenameUnifier::make_bam_filename(alpha_filename);

  return f.get_fullpath() + ":" + a.get_fullpath() + ":" +
    format_string(alpha_file_channel);
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::register_with_read_factory
//       Access: Public, Static
//  Description: Registers the current object as something that can be
//               read from a Bam file.
////////////////////////////////////////////////////////////////////
void TextureImage::
register_with_read_factory() {
  BamReader::get_factory()->
    register_factory(get_class_type(), make_TextureImage);
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::write_datagram
//       Access: Public, Virtual
//  Description: Fills the indicated datagram up with a binary
//               representation of the current object, in preparation
//               for writing to a Bam file.
////////////////////////////////////////////////////////////////////
void TextureImage::
write_datagram(BamWriter *writer, Datagram &datagram) {
  ImageFile::write_datagram(writer, datagram);
  datagram.add_string(get_name());

  // We don't write out _request; this is re-read from the .txa file
  // each time.

  // We don't write out _pre_txa_properties; this is transitional.

  // We don't write out _preferred_source; this is redetermined each
  // session.

  datagram.add_bool(_is_surprise);
  datagram.add_bool(_ever_read_image);
  datagram.add_bool(_forced_grayscale);
  datagram.add_uint8(_alpha_bits);
  datagram.add_int16((int)_alpha_mode);

  // We don't write out _explicitly_assigned_groups; this is re-read
  // from the .txa file each time.

  _actual_assigned_groups.write_datagram(writer, datagram);

  // We don't write out _egg_files; this is redetermined each session.

  datagram.add_uint32(_placement.size());
  Placement::const_iterator pi;
  for (pi = _placement.begin(); pi != _placement.end(); ++pi) {
    writer->write_pointer(datagram, (*pi).first);
    writer->write_pointer(datagram, (*pi).second);
  }

  datagram.add_uint32(_sources.size());
  Sources::const_iterator si;
  for (si = _sources.begin(); si != _sources.end(); ++si) {
    writer->write_pointer(datagram, (*si).second);
  }

  datagram.add_uint32(_dests.size());
  Dests::const_iterator di;
  for (di = _dests.begin(); di != _dests.end(); ++di) {
    writer->write_pointer(datagram, (*di).second);
  }
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::complete_pointers
//       Access: Public, Virtual
//  Description: Called after the object is otherwise completely read
//               from a Bam file, this function's job is to store the
//               pointers that were retrieved from the Bam file for
//               each pointer object written.  The return value is the
//               number of pointers processed from the list.
////////////////////////////////////////////////////////////////////
int TextureImage::
complete_pointers(TypedWritable **p_list, BamReader *manager) {
  int pi = ImageFile::complete_pointers(p_list, manager);

  pi += _actual_assigned_groups.complete_pointers(p_list + pi, manager);

  int i;
  for (i = 0; i < _num_placement; i++) {
    PaletteGroup *group;
    TexturePlacement *placement;
    DCAST_INTO_R(group, p_list[pi++], pi);
    DCAST_INTO_R(placement, p_list[pi++], pi);
    _placement.insert(Placement::value_type(group, placement));
  }

  for (i = 0; i < _num_sources; i++) {
    SourceTextureImage *source;
    DCAST_INTO_R(source, p_list[pi++], pi);
    string key = get_source_key(source->get_filename(),
                                source->get_alpha_filename(),
                                source->get_alpha_file_channel());

    bool inserted = _sources.insert(Sources::value_type(key, source)).second;
    if (!inserted) {
      nout << "Warning: texture key " << key
           << " is nonunique; texture lost.\n";
    }
  }

  for (i = 0; i < _num_dests; i++) {
    DestTextureImage *dest;
    DCAST_INTO_R(dest, p_list[pi++], pi);
    bool inserted = _dests.insert(Dests::value_type(dest->get_filename(), dest)).second;
    if (!inserted) {
      nout << "Warning: dest filename " << dest->get_filename()
           << " is nonunique; texture lost.\n";
    }
  }

  return pi;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::make_TextureImage
//       Access: Protected
//  Description: This method is called by the BamReader when an object
//               of this type is encountered in a Bam file; it should
//               allocate and return a new object with all the data
//               read.
////////////////////////////////////////////////////////////////////
TypedWritable *TextureImage::
make_TextureImage(const FactoryParams &params) {
  TextureImage *me = new TextureImage;
  DatagramIterator scan;
  BamReader *manager;

  parse_params(params, scan, manager);
  me->fillin(scan, manager);
  return me;
}

////////////////////////////////////////////////////////////////////
//     Function: TextureImage::fillin
//       Access: Protected
//  Description: Reads the binary data from the given datagram
//               iterator, which was written by a previous call to
//               write_datagram().
////////////////////////////////////////////////////////////////////
void TextureImage::
fillin(DatagramIterator &scan, BamReader *manager) {
  ImageFile::fillin(scan, manager);
  set_name(scan.get_string());

  _is_surprise = scan.get_bool();
  _ever_read_image = scan.get_bool();
  _forced_grayscale = scan.get_bool();
  _alpha_bits = scan.get_uint8();
  _alpha_mode = (EggRenderMode::AlphaMode)scan.get_int16();

  _actual_assigned_groups.fillin(scan, manager);

  _num_placement = scan.get_uint32();
  manager->read_pointers(scan, _num_placement * 2);

  _num_sources = scan.get_uint32();
  manager->read_pointers(scan, _num_sources);
  _num_dests = scan.get_uint32();
  manager->read_pointers(scan, _num_dests);
}
