/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include "net/instaweb/rewriter/public/image_combine_filter.h"

#include <cstddef>                     // for size_t
#include <iterator>
#include <map>
#include <set>
#include <utility>
#include <vector>

#include "base/logging.h"
#include "net/instaweb/rewriter/cached_result.pb.h"
#include "net/instaweb/rewriter/input_info.pb.h"
#include "net/instaweb/rewriter/public/css_resource_slot.h"
#include "net/instaweb/rewriter/public/css_util.h"
#include "net/instaweb/rewriter/public/image.h"
#include "net/instaweb/rewriter/public/output_resource.h"
#include "net/instaweb/rewriter/public/output_resource_kind.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/resource_combiner.h"
#include "net/instaweb/rewriter/public/resource_slot.h"
#include "net/instaweb/rewriter/public/rewrite_context.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_result.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/spriter/image_library_interface.h"
#include "net/instaweb/spriter/public/image_spriter.h"
#include "net/instaweb/spriter/public/image_spriter.pb.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/md5_hasher.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/stl_util.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/image_types.pb.h"
#include "util/utf8/public/unicodetext.h"
#include "webutil/css/identifier.h"
#include "webutil/css/parser.h"
#include "webutil/css/property.h"
#include "webutil/css/value.h"

namespace net_instaweb {

typedef std::map<GoogleString, const spriter::Rect*> RectMap;
namespace {

// names for Statistics variables.
const char kImageFileCountReduction[] = "image_file_count_reduction";

}  // namespace

// Unfortunately SpriteFuture can't be inside an anonymous namespace due to
// http://gcc.gnu.org/bugzilla/show_bug.cgi?id=29365, which is relevant to
// some of our supported open-source platforms.
namespace spriter_binding {

// A SpriteFuture keeps track of a single image that is to be sprited.  When
// constructed, it is in an invalid state and merely serves as a token for the
// partnership.
class SpriteFuture {
 public:
  // old_url is the original URL which will be replaced with the sprite.  We
  // keep track of it so that we can avoid putting the same image in the sprite
  // twice.
  // Initialize declarations for future use.
  // Used for async flow where we can't check image against decls
  // when the future is created.
  explicit SpriteFuture(const StringPiece& old_url, int width, int height,
                        Css::Declarations* decls)
      : url_value_(NULL),
        x_value_(NULL),
        y_value_(NULL),
        declarations_(decls),
        declaration_to_push_(NULL),
        div_width_(width),
        div_height_(height),
        has_position_(false) {
    old_url.CopyToString(&old_url_);
    x_offset_ = 0;
    y_offset_ = 0;
  }

  ~SpriteFuture() {}

  // Bind this Future to a particular image.  Owns nothing; the inputs must
  // outlive this future.
  void Initialize(Css::Value* url_value) {
    url_value_ = url_value;
  }

  const GoogleString& old_url() { return old_url_; }

  Css::Declarations* decls() { return declarations_; }

  // Set x_px and y_px to the alignment for this image/div combination
  // before spriting.
  bool SetAlignmentValues(Css::Value* x_value, Css::Value* y_value,
                          int image_width, int image_height,
                          int* x_px, int* y_px) {
    bool ret = true;
    if (x_value->GetLexicalUnitType() == Css::Value::NUMBER) {
      if (IsValidNumberPosition(*x_value)) {
        int int_value = x_value->GetIntegerValue();
        *x_px = int_value;
      } else {
        ret = false;
      }
    } else if (x_value->GetLexicalUnitType() == Css::Value::IDENT) {
      switch (x_value->GetIdentifier().ident()) {
        case Css::Identifier::LEFT:
          *x_px = 0;
          break;
        case Css::Identifier::RIGHT:
          *x_px = div_width_ - image_width;
          break;
        case Css::Identifier::CENTER:
          *x_px = (div_width_ - image_width) / 2;
          break;
        default:
          ret = false;
          break;
      }
    }
    if (y_value->GetLexicalUnitType() == Css::Value::NUMBER) {
      if (ret && IsValidNumberPosition(*y_value)) {
        int int_value = y_value->GetIntegerValue();
        *y_px = int_value;
      } else {
        ret = false;
      }
    } else if (ret && y_value->GetLexicalUnitType() == Css::Value::IDENT) {
      switch (y_value->GetIdentifier().ident()) {
        case Css::Identifier::TOP:
          *y_px = 0;
          break;
        case Css::Identifier::BOTTOM:
          *y_px = div_height_ - image_height;
          break;
        case Css::Identifier::CENTER:
          *y_px = (div_height_ - image_height) / 2;
          break;
        default:
          ret = false;
          break;
      }
    }
    return ret;
  }

  // (1) Figure out what position declaration we have.
  // (2) If we have x, create y, and vice versa.
  // (3) Insert the new value into the values vector.
  bool ReadSingleValue(Css::Values* values, int values_offset,
                       Css::Value** x_value, Css::Value** y_value) {
    std::unique_ptr<Css::Value> extra_value(
        new Css::Value(Css::Identifier::CENTER));
    Css::Value* value = values->at(values_offset);
    if (value->GetLexicalUnitType() == Css::Value::IDENT) {
      switch (value->GetIdentifier().ident()) {
        case Css::Identifier::LEFT:
        case Css::Identifier::RIGHT:
        case Css::Identifier::CENTER:
          *x_value = value;
          *y_value = extra_value.get();
          break;
        case Css::Identifier::TOP:
        case Css::Identifier::BOTTOM:
          *y_value = value;
          *x_value = extra_value.get();
          break;
        default:
          return false;
      }
    } else {
      // TODO(morlovich): A single-value position is fine, it's
      // the same as if it were followed by center.
      return false;
    }
    values->insert(values->begin() + values_offset + 1, extra_value.release());
    return true;
  }

  // (1) Figure out what position declaration we have first.
  // (2) If horizontal, other is vertical, and vice versa.
  // (3) If first value is a number, second value is vertical,
  //     first is horizontal.
  bool ReadTwoValues(Css::Values* values, int values_offset,
                     Css::Value**x_value, Css::Value** y_value) {
    Css::Value* value = values->at(values_offset);
    Css::Value* other_value = values->at(values_offset + 1);
    if (value->GetLexicalUnitType() == Css::Value::IDENT) {
      switch (value->GetIdentifier().ident()) {
        case Css::Identifier::LEFT:
        case Css::Identifier::RIGHT:
          *x_value = value;
          *y_value = other_value;
          break;
        case Css::Identifier::TOP:
        case Css::Identifier::BOTTOM:
          *x_value = other_value;
          *y_value = value;
          break;
        case Css::Identifier::CENTER:
          if (other_value->GetLexicalUnitType() == Css::Value::IDENT) {
            switch (other_value->GetIdentifier().ident()) {
              case Css::Identifier::LEFT:
              case Css::Identifier::RIGHT:
                *x_value = other_value;
                *y_value = value;
                break;
              case Css::Identifier::TOP:
              case Css::Identifier::BOTTOM:
              case Css::Identifier::CENTER:
                *x_value = value;
                *y_value = other_value;
                break;
              default:
                return false;
            }
          } else {
            // TODO(nforman): Allow for mixing of alignment types,
            // i.e. left 2px.
            return false;
          }
          break;
        default:
          return false;
      }
    } else {
      // If there are two values and neither is an identifier, x comes
      // first: e.g. "5px 6px" means x=5, y=6.
      // TODO(nforman): support % values.
      for (int i = 0; i < 2; ++i) {
        Css::Value* val = values->at(values_offset + i);
        if (val->GetLexicalUnitType() == Css::Value::NUMBER &&
            IsValidNumberPosition(*val)) {
          continue;
        }
        return false;
      }
      *x_value = values->at(values_offset);
      *y_value = values->at(values_offset + 1);
    }
    return true;
  }

  // Attempts to read the x and y values of the background position.  *values
  // is a value array which includes the background-position at values_offset.
  // new_x and new_y are the coordinates of the image in the sprite.  Returns
  // true, and sets up {x,y}_{value,offset}_ if successful.
  bool ReadBackgroundPosition(Css::Values* values, int values_offset,
                              int image_width, int image_height) {
    // Parsing these values is trickier than you might think.  If either
    // of the two values is a non-center identifier, it determines which
    // is x and which is y.  So for example, "5px left" means x=0, y=5 but
    // "5px top" means x=5, y=0.
    // See: http://www.w3.org/TR/CSS21/colors.html#propdef-background-position
    // TODO(abliss): move this to webutil/css?
    Css::Value* x_value = NULL;
    Css::Value* y_value = NULL;
    if ((static_cast<int>(values->size()) - values_offset == 1) ||
        !IsPositionValue(*(values->at(values_offset + 1)))) {
      if (!ReadSingleValue(values, values_offset, &x_value, &y_value)) {
        return false;
      }
    } else if (!ReadTwoValues(values, values_offset, &x_value, &y_value)) {
      return false;
    }
    // Now that we know which value is which dimension, we can extract the
    // values in px.
    int x_px = 0;
    int y_px = 0;
    if (!SetAlignmentValues(x_value, y_value, image_width, image_height,
                            &x_px, &y_px)) {
      return false;
    }
    // When sprited, these x_value_ and y_value_ will both be replaced
    // with absolute pixel values (i.e. not center or left), so they need
    // to be in x-first, y-second order.
    x_value_ = values->at(values_offset);
    x_offset_ = x_px;
    y_value_ = values->at(values_offset + 1);
    y_offset_ = y_px;
    return true;
  }

  // Returns whether or not this is a number value we can handle.
  static bool IsValidNumberPosition(const Css::Value& value) {
    CHECK(value.GetLexicalUnitType() == Css::Value::NUMBER);
    int int_value = value.GetIntegerValue();
    // If the aligment is specified in pixels, or is 0, we can just use it.
    if ((value.GetDimension() == Css::Value::PX) || (int_value == 0)) {
      return true;
    }
    return false;
  }

  // Tries to guess whether this value is an x- or y- position value in the
  // background shorthand value list.
  static bool IsPositionValue(const Css::Value& value) {
    if (value.GetLexicalUnitType() == Css::Value::NUMBER) {
      return true;
    } else if (value.GetLexicalUnitType() == Css::Value::IDENT) {
        switch (value.GetIdentifier().ident()) {
          case Css::Identifier::LEFT:
          case Css::Identifier::RIGHT:
          case Css::Identifier::TOP:
          case Css::Identifier::BOTTOM:
          case Css::Identifier::CENTER:
            return true;
          default:
            return false;
        }
    }
    return false;
  }

  // Attempt to actually perform the url substitution.  Initialize must have
  // been called first, and must have returned true.
  void Realize(const char* url, int x, int y) {
    if (!has_position_) {
      // If no position was specified, it defaults to "0% 0%", which is the same
      // as "0px 0px".
      Css::Values* values = new Css::Values();
      x_value_ = new Css::Value(0, Css::Value::PX);
      values->push_back(x_value_);
      y_value_ = new Css::Value(0, Css::Value::PX);
      values->push_back(y_value_);
      declaration_to_push_ = new Css::Declaration(
          Css::Property::BACKGROUND_POSITION, values, false);
    }
    CHECK(x_value_ != NULL);
    *url_value_ = Css::Value(Css::Value::URI, UTF8ToUnicodeText(url));
    *x_value_ = Css::Value(x_offset_ - x, Css::Value::PX);
    *y_value_ = Css::Value(y_offset_ - y, Css::Value::PX);

    if ((declarations_ != NULL) && (declaration_to_push_ != NULL)) {
      declarations_->push_back(declaration_to_push_);
    }
  }

  int width() {
    return div_width_;
  }

  int height() {
    return div_height_;
  }

  // Attempt to find the background position values, or create them if
  // necessary.  If we return true, we should be all set for a call to
  // Realize().  If we return false, Realize() must never be called.
  // set has_position_ to true if there is already a position declaration.
  // If has_position_ is false, we will create a new declaration when
  // rendering.
  // Returns true if this is a viable sprite-future.  If
  // we return false, Realize must not be called.
  bool FindBackgroundPositionValues(int image_width, int image_height) {
    // Find the original background offsets (if any) so we can add to them.
    has_position_ = false;
    for (Css::Declarations::iterator decl_iter = declarations_->begin();
         !has_position_ && (decl_iter != declarations_->end());
         ++decl_iter) {
      Css::Declaration* decl = *decl_iter;
      switch (decl->prop()) {
        case Css::Property::BACKGROUND_POSITION: {
          Css::Values* decl_values = decl->mutable_values();
          if (decl_values->size() > 2 || decl_values->size() < 1) {
            return false;
          }
          if (ReadBackgroundPosition(decl_values, 0,
                                     image_width, image_height)) {
            has_position_ = true;
          } else {
            // Upon failure here, we abort the sprite.
            return false;
          }
          break;
        }
        case Css::Property::BACKGROUND_POSITION_X:
        case Css::Property::BACKGROUND_POSITION_Y:
          // These are non-standard, though supported in IE and Chrome.
          // TODO(abliss): handle these.
          return false;
        case Css::Property::BACKGROUND: {
          Css::Values* decl_values = decl->mutable_values();
          // The background shorthand can include many values in any order.
          // We'll look for two consecutive position values.  (If only one
          // position value is present, the other is considered to be CENTER
          // which we don't support.)
          for (int i = 0, n = decl_values->size(); i < n; ++i) {
            if (IsPositionValue(*(decl_values->at(i)))) {
              if (ReadBackgroundPosition(decl_values, i,
                                         image_width, image_height)) {
                has_position_ = true;
                break;
              } else {
                // Upon failure here, we abort the sprite.
                return false;
              }
            }
          }
          break;
        }
        default:
          break;
      }
    }
    // TODO(abliss): consider specifying width and height.  Currently we are
    // assuming the node is already sized correctly.
    return true;
  }

 private:
  GoogleString old_url_;
  // Pointer to the value where the url of the image is stored.
  Css::Value* url_value_;
  // Pointer to the value where the background position x coordinate is stored.
  Css::Value* x_value_;
  // Pointer to the value where the background position y coordinate is stored.
  Css::Value* y_value_;
  // Optional pointer to a declarations object where a new declaration will be
  // pushed.
  Css::Declarations* declarations_;
  // Optional pointer to a declaration object to be pushed onto declarations_.
  Css::Declaration* declaration_to_push_;
  int x_offset_;
  int y_offset_;
  // Width and height of original div.  We use these to check against
  // the image's dimensions once the image is loaded, so we can determine
  // if the image can be sprited in this context.
  int div_width_;
  int div_height_;
  bool has_position_;
  DISALLOW_COPY_AND_ASSIGN(SpriteFuture);
};

// An implementation of the Spriter's ImageLibraryInterface on top of our own
// Image class.  Instead of using the filesystem, we keep an in-memory map,
// which owns pointers to images.
class Library : public spriter::ImageLibraryInterface {
 public:
  // A thin layer of glue around an Image as input to the Spriter.
  class SpriterImage : public spriter::ImageLibraryInterface::Image {
   public:
    // Owns nothing.  Image may not be null.  The library is expected to
    // maintain ownership of the image pointer.
    SpriterImage(net_instaweb::Image* image,
                 spriter::ImageLibraryInterface* lib) :
        Image(lib), image_(image) {
      DCHECK(image_ != NULL) << "null image not allowed.";
    }

    virtual ~SpriterImage() {}

    virtual bool GetDimensions(int* out_width, int* out_height) const {
      ImageDim dim;
      image_->Dimensions(&dim);
      *out_width = dim.width();
      *out_height = dim.height();
      return (dim.width() >= 0) && (dim.height() >= 0);
    }

    // TODO(abliss): This should really be returning const Image*.
    net_instaweb::Image* image() const { return image_; }

   private:
    net_instaweb::Image* image_;
    DISALLOW_COPY_AND_ASSIGN(SpriterImage);
  };

  // A thin layer of glue around an Image as output from the Spriter.
  // Owns its own mutable image.
  class Canvas : public spriter::ImageLibraryInterface::Canvas {
   public:
    Canvas(int width, int height, Library* lib,
           const StringPiece& tmp_dir, Timer* timer, MessageHandler* handler) :
        spriter::ImageLibraryInterface::Canvas(lib),
        lib_(lib) {
      DCHECK(lib != NULL);
      net_instaweb::Image::CompressionOptions* options =
          new net_instaweb::Image::CompressionOptions();
      options->recompress_png = true;
      image_.reset(BlankImageWithOptions(width, height,
                                         net_instaweb::IMAGE_PNG,
                                         tmp_dir, timer, handler, options));
    }

    virtual ~Canvas() { }

    virtual bool DrawImage(const Image* image, int x, int y) {
      const SpriterImage* spriter_image
          = static_cast<const SpriterImage*>(image);
      return image_->DrawImage(spriter_image->image(), x, y);
    }

    // On successfully writing, we release our image.
    virtual bool WriteToFile(
        const FilePath& write_path, spriter::ImageFormat format) {
      if (format != spriter::PNG) {
        return false;
      }
      lib_->RegisterImage(write_path, image_.release());
      return true;
    }

   private:
    scoped_ptr<net_instaweb::Image> image_;
    Library* lib_;

    DISALLOW_COPY_AND_ASSIGN(Canvas);
  };

  Library(Delegate* delegate, const StringPiece& tmp_dir,
          Timer* timer, MessageHandler* handler)
      : spriter::ImageLibraryInterface(delegate), timer_(timer),
        handler_(handler) {
    tmp_dir.CopyToString(&tmp_dir_);
  }

  ~Library() {
    STLDeleteValues(&fake_fs_);
  }

  // Read an image from disk.  Return NULL (after calling delegate method) on
  // error.  Caller owns the returned pointer, which must not outlive this
  // library.
  virtual SpriterImage* ReadFromFile(const FilePath& path) {
    net_instaweb::Image* image = fake_fs_[path];
    if (image == NULL) {
      return NULL;
    }
    return new SpriterImage(image, this);
  }

  virtual Canvas* CreateCanvas(int width, int height) {
    return new Canvas(width, height, this, tmp_dir_, timer_, handler_);
  }

  // Does not take ownership of the resource.  Returns true if the image could
  // be detected as a valid format, in which case we'll keep our own pointer to
  // the image backed by the resource, meaning that resource must not be
  // destroyed before the next call to Clear().
  bool Register(Resource* resource, MessageHandler* handler) {
    net_instaweb::Image* prev_image = fake_fs_[resource->url()];
    if (prev_image != NULL) {
      // Already registered
      return true;
    }

    net_instaweb::Image::CompressionOptions* image_options =
        new net_instaweb::Image::CompressionOptions();
    // TODO(satyanarayana): Use appropriate quality param for spriting.
    image_options->jpeg_quality =
        RewriteOptions::kDefaultImageJpegRecompressQuality;
    // TODO(nikhilmadan): Use appropriate progressive setting for spriting.
    image_options->progressive_jpeg = false;

    scoped_ptr<net_instaweb::Image> image(
        NewImage(resource->ExtractUncompressedContents(), resource->url(),
                 tmp_dir_, image_options, timer_, handler_));

    // We only handle PNGs and GIFs (which are converted to PNGs) for now.
    net_instaweb::ImageType image_type = image->image_type();
    if ((image_type != net_instaweb::IMAGE_PNG) &&
        (image_type != net_instaweb::IMAGE_GIF)) {
      handler->Message(kInfo, "Cannot sprite: not PNG or GIF, %s",
                       resource->url().c_str());
      return false;
    }
    RegisterImage(resource->url(), image.release());
    return true;
  }

  void Clear() {
    STLDeleteValues(&fake_fs_);
    fake_fs_.clear();
  }

 private:
  typedef std::map<const GoogleString, net_instaweb::Image*> ImageMap;
  void RegisterImage(const StringPiece& key, net_instaweb::Image* image) {
    std::pair<ImageMap::iterator, bool> result(
        fake_fs_.insert(std::make_pair(key.as_string(), image)));
    if (!result.second) {
      // Already existed.
      ImageMap::iterator iter = result.first;
      if (iter->second != image) {
        delete iter->second;
        iter->second = image;
      }
    }
  }

  // The spriter expects a filesystem interface for accessing images, but we
  // don't want to hit the disk excessively.  We keep here an in-memory map from
  // a "pathname" to its Image (which contains both the encoded input and the
  // decoded raster) for quick access. Owns the Image objects.
  ImageMap fake_fs_;
  GoogleString tmp_dir_;
  Timer* timer_;
  MessageHandler* handler_;
};

}  // namespace spriter_binding

using spriter_binding::Library;
using spriter_binding::SpriteFuture;

// The Combiner does all the work of spriting.  Each combiner takes a set of
// images and produces a single sprite as a combination.
class ImageCombineFilter::Combiner : public ResourceCombiner {
 public:
  Combiner(ImageCombineFilter* filter, Library* library)
      // TODO(jmaessen): The addition of 1 below avoids the leading ".";
      // make this convention consistent and fix all code.
      : ResourceCombiner(filter->driver(), kContentTypePng.file_extension() + 1,
                         filter),
        library_(library) { }

  virtual ~Combiner() {
    // Note that the superclass's dtor will not call our overridden Clear.
    // Fortunately there's no harm in calling Clear() several times.
    Clear();
  }

  virtual bool WriteCombination(
      const ResourceVector& combine_resources,
      const OutputResourcePtr& combination,
      MessageHandler* handler) {
    spriter::ImageSpriter spriter(library_);

    spriter::SpriterInput input;
    input.set_id(0);
    spriter::SpriteOptions* options = input.mutable_options();
    options->set_output_base_path("");
    options->set_output_image_path("sprite");
    options->set_placement_method(spriter::VERTICAL_STRIP);

    for (int i = 0, n = combine_resources.size(); i < n; ++i) {
      const ResourcePtr& resource = combine_resources[i];
      input.add_input_image_set()->set_path(resource->url());
    }

    scoped_ptr<spriter::SpriterResult> result(spriter.Sprite(input));
    if (result.get() == NULL) {
      handler->Error(UrlSafeId().c_str(), 0,
                     "Could not sprite.");
      return false;
    }
    scoped_ptr<Library::SpriterImage> result_image(
        library_->ReadFromFile(result->output_image_path()));
    if (result_image.get() == NULL) {
      handler->Error(UrlSafeId().c_str(), 0,
                     "Could not read sprited image.");
      return false;
    }

    combination->EnsureCachedResultCreated()->mutable_spriter_result()->
        CopyFrom(*result);
    if (!rewrite_driver_->Write(combine_resources,
                                result_image->image()->Contents(),
                                &kContentTypePng,
                                StringPiece(),  // no charset on images.
                                combination.get())) {
      handler->Error(UrlSafeId().c_str(), 0,
                     "Could not write sprited resource.");
      return false;
    }
    return true;
  }

  OutputResourcePtr MakeOutput() {
    return Combine(rewrite_driver_->message_handler());
  }

  virtual void Clear() {
    ResourceCombiner::Clear();
    added_urls_.clear();
  }

  bool Write(const ResourceVector& in, const OutputResourcePtr& out) {
    return WriteCombination(in, out, rewrite_driver_->message_handler());
  }

 private:
  virtual const ContentType* CombinationContentType() {
    return &kContentTypePng;
  }

  StringSet added_urls_;
  Library* library_;
};

// Special resource slot that has a future_ pointer.
class SpriteFutureSlot : public CssResourceSlot {
 public:
  SpriteFutureSlot(const ResourcePtr& resource,
                   const GoogleUrl& base_url, const RewriteOptions* options,
                   Css::Values* values, size_t value_index,
                   SpriteFuture* future)
      : CssResourceSlot(resource, base_url, options, values, value_index),
        future_(future),
        may_sprite_(false) {
  }

  SpriteFuture* future() { return future_.get(); }

  virtual void Render() {
    // If we couldn't sprite this slot, try to apply other filters.
    if (!may_sprite_) {
      CssResourceSlot::Render();
    }
  }

  void set_may_sprite(bool x) { may_sprite_ = x; }

  bool may_sprite() const { return may_sprite_; }

 protected:
  REFCOUNT_FRIEND_DECLARATION(SpriteFutureSlot);
  virtual ~SpriteFutureSlot() {
  }

 private:
  scoped_ptr<SpriteFuture> future_;
  bool may_sprite_;
  DISALLOW_COPY_AND_ASSIGN(SpriteFutureSlot);
};

typedef RefCountedPtr<SpriteFutureSlot> SpriteFutureSlotPtr;

class ImageCombineFilter::Context : public RewriteContext {
 public:
  Context(ImageCombineFilter* filter, RewriteContext* parent,
          const GoogleUrl& css_url, const StringPiece& css_text)
      : RewriteContext(NULL, parent, NULL),
        library_(NULL,
                 filter->driver()->server_context()->filename_prefix(),
                 filter->driver()->timer(),
                 filter->driver()->message_handler()),
        filter_(filter) {
    css_base_url_.Reset(css_url);
    MD5Hasher hasher;
    key_suffix_ = StrCat("css-key=", hasher.Hash(css_text),
                         "_", hasher.Hash(css_url.AllExceptLeaf()));
  }

  Context(RewriteDriver* driver, ImageCombineFilter* filter)
      : RewriteContext(driver, NULL, NULL),
        library_(NULL,
                 filter->driver()->server_context()->filename_prefix(),
                 filter->driver()->timer(),
                 filter->driver()->message_handler()),
        filter_(filter) {
  }

  virtual ~Context() {}

  // This cache key will no longer match the partition key generated for
  // fetches in RewriteContext.  This may or may not cause cache misses
  // when we could have had hits. It should not cause any functional
  // errors.
  // We hash the usual cache key, which is a list of the urls in the
  // combination, in order to keep it short so it doesn't run up against
  // filename length limits on apache.
  // TODO(nforman): Figure out a way to test cache keys in general.
  virtual GoogleString CacheKeySuffix() const {
    return key_suffix_;
  }

  bool AddFuture(CssResourceSlotPtr slot) {
    SpriteFutureSlot* future_slot = static_cast<SpriteFutureSlot*>(slot.get());
    StringPiece url(future_slot->future()->old_url());
    AddSlot(ResourceSlotPtr(slot));
    return true;
  }

  virtual const UrlSegmentEncoder* encoder() const {
    return filter_->encoder();
  }
  virtual const char* id() const { return filter_->id(); }
  virtual OutputResourceKind kind() const { return kRewrittenResource; }

  void Reset() {
    library_.Clear();
  }

 protected:
  // Write the combination out.
  virtual void Rewrite(int partition_index, CachedResult* partition,
                       const OutputResourcePtr& output) {
    RewriteResult result = kRewriteOk;
    if (!output->IsWritten()) {
      // Note that this method expects to do something for only the fetch path,
      // when only one partition should be in use --- in the rewrite path
      // we should have already written everything out in Partition().
      DCHECK_EQ(0, partition_index);
      ImageCombineFilter::Combiner combiner(filter_, &library_);

      ResourceVector resources;
      bool ok = true;
      for (int i = 0, n = num_slots(); (i < n) && ok; ++i) {
        ResourcePtr resource(slot(i)->resource());
        resources.push_back(resource);
        RegisterResource(resource.get());
        ok = EnsureLoaded(resource->url());
      }
      if (!ok || !combiner.Write(resources, output)) {
        result = kRewriteFailed;
      }
    }
    RewriteDone(result, partition_index);
  }

  bool PolicyPermitsRendering() const {
    return AreOutputsAllowedByCsp(CspDirective::kImgSrc);
  }

  // Finalize the declarations for the sprited slots.
  // TODO(nforman): be smarter about when to sprite and when not.
  // e.g. if it turns out all the divs are too big to use the sprite
  // except for one, don't use it.
  virtual void Render() {
    for (int p = 0, np = num_output_partitions(); p < np; ++p) {
      const CachedResult* partition = output_partition(p);
      int num_inputs = partition->input_size();
      if (num_inputs > 1) {
        if (!partition->has_spriter_result()) {
          // TODO(nforman): some error handling here.
          LOG(DFATAL) << "spriting failed during Render";
          break;
        }
        const spriter::SpriterResult& spriter_result =
            partition->spriter_result();
        RectMap url_to_clip_rect;
        // Now gather up the positions for each of the original urls.
        for (int i = spriter_result.image_position_size() - 1; i >= 0; i--) {
          const spriter::ImagePosition& image_position =
              spriter_result.image_position(i);
          // Where the spriter expects file paths, we are using urls.
          url_to_clip_rect[image_position.path()] = &image_position.clip_rect();
        }

        StringSet replaced_urls;  // for stats purposes
        for (int i = 0; i < num_inputs; ++i) {
          int slot_index = partition->input(i).index();
          SpriteFutureSlot* sprite_slot =
              static_cast<SpriteFutureSlot*>(slot(slot_index).get());
          SpriteFuture* future = sprite_slot->future();
          const spriter::Rect* clip_rect = url_to_clip_rect[future->old_url()];
          // Check against original image dimensions.
          // If these are smaller than the div we're putting the image
          // into then we can't sprite this declaraion
          if (clip_rect->width() < future->width() ||
              clip_rect->height() < future->height()) {
            continue;
          }
          if (clip_rect != NULL) {
            DCHECK(css_base_url_.IsAnyValid());
            GoogleString new_url;
            if (css_base_url_.IsAnyValid()) {
              new_url = ResourceSlot::RelativizeOrPassthrough(
                  filter_->driver()->options(), partition->url(),
                  sprite_slot->url_relativity(), css_base_url_);
            } else {
              new_url = partition->url();
            }

            future->Realize(new_url.c_str(), clip_rect->x_pos(),
                            clip_rect->y_pos());
            MessageHandler* handler = filter_->driver()->message_handler();
            handler->Message(kInfo, "Inserted sprite, url: %s\n",
                             new_url.c_str());
            replaced_urls.insert(future->old_url());
            sprite_slot->set_may_sprite(true);
          }
        }
        int sprited = replaced_urls.size();
        filter_->AddFilesReducedStat(sprited - 1);
      }
    }
    Reset();
  }

  // Partition the slots by what can get sprited and what can't.
  // Currently, we greedily combine everything that can be combined
  // in as few partitions as possible. We skip over slots that point to images
  // that are too small for the context they're in.
  // TODO(nforman): Consider separating by color map to group things smarter.
  virtual void PartitionAsync(OutputPartitions* partitions,
                              OutputResourceVector* outputs) {
    // Partitioning here requires image decompression, so we want to
    // move it to a different thread.
    Driver()->AddLowPriorityRewriteTask(MakeFunction(
        this, &Context::PartitionImpl, &Context::PartitionCancel,
        partitions, outputs));
  }

  void PartitionImpl(OutputPartitions* partitions,
                     OutputResourceVector* outputs) {
    StringSet no_sprite;
    FindUnspritable(&no_sprite);
    CollectSlots(partitions, outputs, &no_sprite);
    CrossThreadPartitionDone(partitions->partition_size() != 0 ?
                                 kRewriteOk : kRewriteFailed);
  }

  void PartitionCancel(OutputPartitions* partitions,
                       OutputResourceVector* outputs) {
    CrossThreadPartitionDone(kTooBusy);
  }

 private:
  // Class that associates a list of urls and a partition with a combiner.
  class ImageCombination : public ImageCombineFilter::Combiner {
   public:
    ImageCombination(ImageCombineFilter* filter, Library* library)
        : ImageCombineFilter::Combiner(filter, library),
          partition_(NULL) { }

    virtual ~ImageCombination() { }

    void AddResourceToPartition(Resource* resource, int index) {
      resource->AddInputInfoToPartition(
          Resource::kIncludeInputHash, index, partition_);
    }

    void set_partition(CachedResult* partition) { partition_ = partition; }

    CachedResult* partition() { return partition_; }

   private:
    CachedResult* partition_;  // Does not own memory.
    DISALLOW_COPY_AND_ASSIGN(ImageCombination);
  };

  typedef std::vector<ImageCombination*> ImageCombinationVector;

  // Put this resource in the library.
  bool RegisterResource(Resource* resource) {
    return library_.Register(resource, filter_->driver()->message_handler());
  }

  bool EnsureLoaded(const GoogleString& url) {
    scoped_ptr<Library::SpriterImage> spriter_image(library_.ReadFromFile(url));
    if (spriter_image.get() == NULL) {
      return false;
    }

    return spriter_image->image()->EnsureLoaded(false);
  }

  // Returns true if the image at url has already been added to the collection
  // and is at least as large as the given dimensions.
  bool GetImageDimensions(const GoogleString& url, int* width, int* height) {
    scoped_ptr<Library::SpriterImage> image(library_.ReadFromFile(url));
    if (image.get() == NULL) {
      return false;
    }
    return image->GetDimensions(width, height);
  }

  // Returns true iff declarations were setup properly, and the image
  // are smaller than the specified div dimensions.
  bool SetupSpriteDimensions(SpriteFuture* future) {
    int image_width, image_height;
    if (!GetImageDimensions(future->old_url(), &image_width, &image_height)) {
      return false;
    }
    if (image_width < future->width() || image_height < future->height()) {
      return false;
    }
    return future->FindBackgroundPositionValues(image_width, image_height);
  }

  // Walk through and find any resources that won't be able to be
  // sprited.  If we can't sprite them, add the url to the no-sprite
  // set.
  //
  // TODO(abliss) We exhibit zero intelligence about which image files to
  // combine; we combine whatever is possible.  This can reduce cache
  // effectiveness by combining highly cacheable shared resources with
  // transient ones.
  void FindUnspritable(StringSet* no_sprite) {
    StringSet seen_urls;
    for (int i = 0, n = num_slots(); i < n; ++i) {
      ResourcePtr resource(slot(i)->resource());
      SpriteFutureSlot* sprite_slot =
          static_cast<SpriteFutureSlot*>(slot(i).get());
      SpriteFuture* future = sprite_slot->future();
      GoogleString resource_url = resource->url();
      if (no_sprite->find(resource_url) == no_sprite->end()) {
        if (!resource->IsSafeToRewrite(rewrite_uncacheable())) {
          no_sprite->insert(resource_url);
        } else {
          // Register the resource with the library and then check
          // its dimensions against those of the declaration to make
          // sure we can sprite here.
          // TODO(nforman): cheaper image dimension checking.
          if (seen_urls.find(resource_url) == seen_urls.end()) {
            RegisterResource(resource.get());
            seen_urls.insert(resource_url);
          }
          if (!SetupSpriteDimensions(future) || !EnsureLoaded(resource_url)) {
            no_sprite->insert(resource_url);
          }
        }
      }
    }
  }

  // For each slot, try to add its resource to the current partition.
  // If we can't, then finalize the last combination, and then
  // move on to the next slot.
  void CollectSlots(OutputPartitions* partitions,
                    OutputResourceVector* outputs,
                    StringSet* no_sprite) {
    ImageCombinationVector combinations;
    MessageHandler* handler = filter_->driver()->message_handler();
    std::map<GoogleString, ImageCombination*> urls_to_combos;

    for (int i = 0, n = num_slots(); i < n; ++i) {
      ResourcePtr resource(slot(i)->resource());
      SpriteFutureSlot* sprite_slot =
              static_cast<SpriteFutureSlot*>(slot(i).get());
      SpriteFuture* future = sprite_slot->future();
      GoogleString resource_url = future->old_url();
      if (no_sprite->find(resource_url) != no_sprite->end()) {
        continue;
      }
      bool added = false;
      // Don't add the same url to a combination twice.

      std::map<GoogleString, ImageCombination*>::iterator it;
      it = urls_to_combos.find(resource_url);
      if (it != urls_to_combos.end()) {
        ImageCombination* combo = it->second;
        DCHECK(combo != NULL) << "Combination points to NULL partition.";
        combo->AddResourceToPartition(resource.get(), i);
        added = true;
      }
      // If it wasn't already in the combination, see if we can add it.
      // Even if the image is spritable, it may not be able to be sprited
      // in a particular combination due to domain lawyer restrictions,
      // or (perhaps in the future) image type.
      if (!added) {
        for (int j = 0, m = combinations.size(); j < m; ++j) {
          ImageCombination* combo = combinations[j];
          if (combo->AddResourceNoFetch(resource, handler).value) {
            combo->AddResourceToPartition(resource.get(), i);
            urls_to_combos[resource_url] = combo;
            added = true;
            break;
          }
        }
        // If we couldn't add this resource to any of the existing partitions,
        // try making a new one and adding it there.  We may still not be able
        // to do that if the resource isn't spritable at all.
        if (!added) {
          scoped_ptr<ImageCombination> combo(new ImageCombination(
              filter_, &library_));
          if (combo->AddResourceNoFetch(resource, handler).value) {
            combo->set_partition(partitions->add_partition());
            combo->AddResourceToPartition(resource.get(), i);
            urls_to_combos[resource_url] = combo.get();
            combinations.push_back(combo.release());
          } else {
            no_sprite->insert(resource_url);
          }
        }
      }
    }
    FinalizePartitions(combinations, partitions, outputs);
    STLDeleteElements(&combinations);
    Reset();
  }

  // Write the output for the combinations.  If a combination can not be
  // written (e.g. it has only one element), then remove its partition.
  void FinalizePartitions(const ImageCombinationVector& combinations,
                          OutputPartitions* partitions,
                          OutputResourceVector* outputs) {
    std::set<int> remove_indices;
    for (int i = 0, n = combinations.size(); i < n; ++i) {
      ImageCombination* combination = combinations[i];
      CachedResult* partition = combination->partition();
      if (partition != NULL) {
        OutputResourcePtr combination_output(combination->MakeOutput());
        if (combination_output.get() == NULL) {
          remove_indices.insert(i);
        } else {
          combination_output->UpdateCachedResultPreservingInputInfo(partition);
          outputs->push_back(combination_output);
        }
      }
    }
    // We can re-arrange the partitions at this point only because we are
    // about to delete the ImageCombinations (and with them, their pointers
    // to those partitions).
    std::set<int>::reverse_iterator rit;
    for (rit = remove_indices.rbegin(); rit != remove_indices.rend(); rit++) {
      int last_partition = partitions->partition_size() - 1;
      if (*rit != last_partition) {
        partitions->mutable_partition()->SwapElements(*rit, last_partition);
      }
      partitions->mutable_partition()->RemoveLast();
    }
  }

  Library library_;
  ImageCombineFilter* filter_;
  GoogleString key_suffix_;
  // Only stored when actually rewriting CSS. Not for reconstruction requests
  // for the image .pagespeed. sprite itself.
  GoogleUrl css_base_url_;
};

ImageCombineFilter::ImageCombineFilter(RewriteDriver* driver)
    : RewriteFilter(driver),
      context_(NULL) {
  Statistics* stats = driver->server_context()->statistics();
  image_file_count_reduction_ = stats->GetVariable(kImageFileCountReduction);
}

ImageCombineFilter::~ImageCombineFilter() {
}

void ImageCombineFilter::InitStats(Statistics* statistics) {
  statistics->AddVariable(kImageFileCountReduction);
}

// Get the dimensions of the declaration.  This is tricky.
// If the element is larger than the image, spriting will not work correctly.
// TODO(abliss): support same-sized vertically-repeating backgrounds in a
// horizontal sprite, and horizontal ones in a vertical sprite.
bool ImageCombineFilter::GetDeclarationDimensions(
    Css::Declarations* declarations, int* width, int* height) {
  css_util::DimensionState state =
      css_util::GetDimensions(declarations, width, height);
  return (state == css_util::kHasBothDimensions);
}

// Must initialize context_ with appropriate parent before hand.
// parent passed here because it's private.
bool ImageCombineFilter::AddCssBackgroundContext(
    const GoogleUrl& original_url, const GoogleUrl& base_url,
    Css::Values* values, int value_index,
    CssFilter::Context* parent, Css::Declarations* decls,
    bool* is_authorized, MessageHandler* handler) {
  CHECK(context_ != NULL);
  *is_authorized = true;  // Only false if original_url isn't authorized.
  int width, height;
  if (!GetDeclarationDimensions(decls, &width, &height)) {
    return false;
  }
  StringPiece url_piece(original_url.Spec());
  scoped_ptr<SpriteFuture> future(new SpriteFuture(url_piece, width, height,
                                                   decls));
  future->Initialize(values->at(value_index));

  ResourcePtr resource = CreateInputResource(
      url_piece, RewriteDriver::InputRole::kImg, is_authorized);
  if (resource.get() == NULL) {
    return false;
  }

  // transfers ownership of future to slot_obj
  SpriteFutureSlot* slot_obj = new SpriteFutureSlot(
      resource, base_url, driver()->options(), values, value_index,
      future.release());
  CssResourceSlotPtr slot(slot_obj);
  parent->slot_factory()->UniquifySlot(slot);
  // Spriting must run before all other filters so that the slot for the
  // resource a SpriteFutureSlot
  if (slot.get() != slot_obj) {
    return false;
  }
  context_->AddFuture(slot);
  return true;
}

void ImageCombineFilter::Reset(RewriteContext* parent,
                               const GoogleUrl& css_url,
                               const StringPiece& css_text) {
  context_ = MakeNestedContext(parent, css_url, css_text);
}

void ImageCombineFilter::RegisterOrReleaseContext() {
  if ((context_ != NULL) && (context_->num_slots() != 0)) {
    context_->parent()->AddNestedContext(context_);
  } else {
    delete context_;
    context_ = NULL;
  }
}

// Make a new context that is nested under parent.
ImageCombineFilter::Context* ImageCombineFilter::MakeNestedContext(
    RewriteContext* parent, const GoogleUrl& css_url,
    const StringPiece& css_text) {
  Context* context = new Context(this, parent, css_url, css_text);
  return context;
}

RewriteContext* ImageCombineFilter::MakeRewriteContext() {
  return new Context(driver(), this);
}

void ImageCombineFilter::AddFilesReducedStat(int reduced) {
  image_file_count_reduction_->Add(reduced);
}

}  // namespace net_instaweb
