// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/app_list/model/folder_image.h"

#include <memory>
#include <vector>

#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_item_list.h"
#include "ash/public/cpp/app_list/app_list_color_provider.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_config_provider.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/scoped_canvas.h"

namespace ash {

namespace {

// The shadow blur of icon.
constexpr int kIconShadowBlur = 5;

// The shadow color of icon.
constexpr SkColor kIconShadowColor = SkColorSetA(SK_ColorBLACK, 31);

// The default bubble color.
constexpr SkColor kDefaultBubbleColor = SkColorSetA(gfx::kGoogleGrey100, 0x7A);

// Generates the folder icon with the top 4 child item icons laid in 2x2 tile.
class FolderImageSource : public gfx::CanvasImageSource {
 public:
  typedef std::vector<gfx::ImageSkia> Icons;

  FolderImageSource(const AppListConfig& app_list_config,
                    const Icons& icons,
                    const gfx::Size& size);
  ~FolderImageSource() override;

 private:
  void DrawIcon(gfx::Canvas* canvas,
                const gfx::ImageSkia& icon,
                const gfx::Size icon_size,
                int x,
                int y);

  // gfx::CanvasImageSource overrides:
  void Draw(gfx::Canvas* canvas) override;

  const AppListConfig& app_list_config_;
  Icons icons_;
  gfx::Size size_;

  DISALLOW_COPY_AND_ASSIGN(FolderImageSource);
};

FolderImageSource::FolderImageSource(const AppListConfig& app_list_config,
                                     const Icons& icons,
                                     const gfx::Size& size)
    : gfx::CanvasImageSource(size),
      app_list_config_(app_list_config),
      icons_(icons),
      size_(size) {
  DCHECK(icons.size() <= FolderImage::kNumFolderTopItems);
}

FolderImageSource::~FolderImageSource() = default;

void FolderImageSource::DrawIcon(gfx::Canvas* canvas,
                                 const gfx::ImageSkia& icon,
                                 const gfx::Size icon_size,
                                 int x,
                                 int y) {
  if (icon.isNull())
    return;

  const gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage(
      icon, skia::ImageOperations::RESIZE_BEST, icon_size));

  // Draw a shadowed icon on the specified location.
  const gfx::ShadowValues shadow = {
      gfx::ShadowValue(gfx::Vector2d(), kIconShadowBlur, kIconShadowColor)};
  const gfx::ImageSkia shadowed(
      gfx::ImageSkiaOperations::CreateImageWithDropShadow(resized, shadow));

  // Offset the shadowed image so the actual image position matches its original
  // bounds. The offset has to be calculated in pixels, as the shadow margin
  // might not match the 1x margin scaled to the target scale factor (when the
  // drawing the shadow, the shadow margin is first scaled then rounded, which
  // might introduce different rounding error depending on the scale factor).
  gfx::ScopedCanvas scoped_canvas(canvas);
  const float scale = canvas->UndoDeviceScaleFactor();

  gfx::Insets shadow_margin =
      gfx::ShadowValue::GetMargin({shadow[0].Scale(scale)});
  const gfx::ImageSkiaRep& shadowed_rep = shadowed.GetRepresentation(scale);

  canvas->DrawImageIntInPixel(
      shadowed_rep, x * scale + shadow_margin.left(),
      y * scale + shadow_margin.top(), scale * shadowed.width(),
      scale * shadowed.height(), true, cc::PaintFlags());
}

void FolderImageSource::Draw(gfx::Canvas* canvas) {
  gfx::PointF bubble_center(size().width() / 2, size().height() / 2);
  bubble_center.Offset(0, -app_list_config_.folder_bubble_y_offset());

  // Draw circle for folder bubble.
  cc::PaintFlags flags;
  flags.setStyle(cc::PaintFlags::kFill_Style);
  flags.setAntiAlias(true);
  flags.setColor(AppListColorProvider::Get()
                     ? AppListColorProvider::Get()->GetFolderBubbleColor()
                     : kDefaultBubbleColor);
  canvas->DrawCircle(bubble_center, app_list_config_.folder_bubble_radius(),
                     flags);

  if (icons_.size() == 0)
    return;

  // Draw top items' icons.
  const size_t num_items =
      std::min(FolderImage::kNumFolderTopItems, icons_.size());
  std::vector<gfx::Rect> top_icon_bounds = FolderImage::GetTopIconsBounds(
      app_list_config_, gfx::Rect(size()), num_items);

  for (size_t i = 0; i < num_items; ++i) {
    DrawIcon(canvas, icons_[i],
             app_list_config_.item_icon_in_folder_icon_size(),
             top_icon_bounds[i].x(), top_icon_bounds[i].y());
  }
}

}  // namespace

// static
const size_t FolderImage::kNumFolderTopItems = 4;

FolderImage::FolderImage(const AppListConfig* app_list_config,
                         AppListItemList* item_list)
    : app_list_config_(app_list_config), item_list_(item_list) {
  DCHECK(app_list_config_);
  item_list_->AddObserver(this);
}

FolderImage::~FolderImage() {
  for (auto* item : top_items_)
    item->RemoveObserver(this);
  item_list_->RemoveObserver(this);
}

void FolderImage::UpdateIcon() {
  for (auto* item : top_items_)
    item->RemoveObserver(this);
  top_items_.clear();

  for (size_t i = 0;
       i < item_list_->item_count() && top_items_.size() < kNumFolderTopItems;
       ++i) {
    AppListItem* item = item_list_->item_at(i);
    // If this item is currently being dragged, pretend it has already left our
    // folder
    if (item == dragged_item_)
      continue;
    item->AddObserver(this);
    top_items_.push_back(item);
  }
  RedrawIconAndNotify();
}

void FolderImage::UpdateDraggedItem(const AppListItem* dragged_item) {
  DCHECK(dragged_item_ != dragged_item);
  dragged_item_ = dragged_item;
  UpdateIcon();
}

// static
std::vector<gfx::Rect> FolderImage::GetTopIconsBounds(
    const AppListConfig& app_list_config,
    const gfx::Rect& folder_icon_bounds,
    size_t num_items) {
  DCHECK_LE(num_items, kNumFolderTopItems);
  std::vector<gfx::Rect> top_icon_bounds;

  const AppListConfig& base_config =
      app_list_config.type() == AppListConfigType::kShared
          ? AppListConfig::instance()
          : *AppListConfigProvider::Get().GetConfigForType(
                app_list_config.type(), true /*can_create*/);

  // The folder icons are generated as unclipped icons for default app list
  // config, and then scaled down to the required unclipped folder size as
  // needed (if clipped icon is needed, the unclipped icon bounds are clipped to
  // the target size).
  // This method goes through a similar flow:
  // 1.   Calculate the top icon bounds in the default unclipped folder icon.
  // 2.   Scale the bounds to the target config unclipped folder icon size.
  // 3.   Translate the bound to adjust for clipped bounds size (expected to be
  //      the |folder_icon_bounds| size).
  // 4.   Translate to bounds to adjust for the clipped bounds origin (expected
  //      to be the |folder_icon_bounds| origin).
  // Steps 2 - 4 are done using |scale_and_translate_bounds|.
  const int item_icon_dimension =
      base_config.item_icon_in_folder_icon_dimension();
  const int folder_unclipped_icon_dimension =
      base_config.folder_unclipped_icon_dimension();
  gfx::Point icon_center(folder_unclipped_icon_dimension / 2,
                         folder_unclipped_icon_dimension / 2);
  const gfx::Rect center_rect(icon_center.x() - item_icon_dimension / 2,
                              icon_center.y() - item_icon_dimension / 2,
                              item_icon_dimension, item_icon_dimension);

  const int origin_offset =
      (item_icon_dimension + base_config.item_icon_in_folder_icon_margin()) / 2;

  const int scaled_folder_unclipped_icon_dimension =
      app_list_config.folder_unclipped_icon_dimension();
  auto scale_and_translate_bounds = [folder_icon_bounds,
                                     folder_unclipped_icon_dimension,
                                     scaled_folder_unclipped_icon_dimension](
                                        const gfx::Rect& original) {
    const float scale =
        static_cast<float>(scaled_folder_unclipped_icon_dimension) /
        folder_unclipped_icon_dimension;
    gfx::Rect bounds = gfx::ScaleToRoundedRect(original, scale, scale);
    const int clipped_image_offset =
        (scaled_folder_unclipped_icon_dimension - folder_icon_bounds.width()) /
        2;
    bounds.Offset(-clipped_image_offset, -clipped_image_offset);
    bounds.Offset(folder_icon_bounds.x(), folder_icon_bounds.y());
    return bounds;
  };

  if (num_items == 1) {
    // Center icon bounds.
    top_icon_bounds.emplace_back(scale_and_translate_bounds(center_rect));
    return top_icon_bounds;
  }

  if (num_items == 2) {
    // Left icon bounds.
    gfx::Rect left_rect = center_rect;
    left_rect.Offset(-origin_offset, 0);
    top_icon_bounds.emplace_back(scale_and_translate_bounds(left_rect));

    // Right icon bounds.
    gfx::Rect right_rect = center_rect;
    right_rect.Offset(origin_offset, 0);
    top_icon_bounds.emplace_back(scale_and_translate_bounds(right_rect));
    return top_icon_bounds;
  }

  // Top left icon bounds.
  gfx::Rect top_left_rect = center_rect;
  top_left_rect.Offset(-origin_offset, -origin_offset);
  top_icon_bounds.emplace_back(scale_and_translate_bounds(top_left_rect));

  // Top right icon bounds.
  gfx::Rect top_right_rect = center_rect;
  top_right_rect.Offset(origin_offset, -origin_offset);
  top_icon_bounds.emplace_back(scale_and_translate_bounds(top_right_rect));

  if (num_items == 3) {
    // Bottom icon bounds.
    gfx::Rect bottom_rect = center_rect;
    bottom_rect.Offset(0, origin_offset);
    top_icon_bounds.emplace_back(scale_and_translate_bounds(bottom_rect));
    return top_icon_bounds;
  }

  // Bottom left icon bounds.
  gfx::Rect bottom_left_rect = center_rect;
  bottom_left_rect.Offset(-origin_offset, origin_offset);
  top_icon_bounds.emplace_back(scale_and_translate_bounds(bottom_left_rect));

  // Bottom right icon bounds.
  gfx::Rect bottom_right_rect = center_rect;
  bottom_right_rect.Offset(origin_offset, origin_offset);
  top_icon_bounds.emplace_back(scale_and_translate_bounds(bottom_right_rect));
  return top_icon_bounds;
}

gfx::Rect FolderImage::GetTargetIconRectInFolderForItem(
    const AppListConfig& app_list_config,
    AppListItem* item,
    const gfx::Rect& folder_icon_bounds) const {
  for (size_t i = 0; i < top_items_.size(); ++i) {
    if (item->id() == top_items_[i]->id()) {
      std::vector<gfx::Rect> rects = GetTopIconsBounds(
          app_list_config, folder_icon_bounds, top_items_.size());
      return rects[i];
    }
  }

  gfx::Rect target_rect(folder_icon_bounds);
  target_rect.ClampToCenteredSize(
      app_list_config.item_icon_in_folder_icon_size());
  return target_rect;
}

void FolderImage::AddObserver(FolderImageObserver* observer) {
  observers_.AddObserver(observer);
}

void FolderImage::RemoveObserver(FolderImageObserver* observer) {
  observers_.RemoveObserver(observer);
}

void FolderImage::ItemIconChanged(AppListConfigType config_type) {
  if (config_type != AppListConfigType::kShared &&
      config_type != app_list_config_->type()) {
    return;
  }

  // Note: Must update the image only (cannot simply call UpdateIcon), because
  // UpdateIcon removes and re-adds the FolderImage as an observer of the
  // AppListItems, which causes the current iterator to call ItemIconChanged
  // again, and goes into an infinite loop.
  RedrawIconAndNotify();
}

void FolderImage::OnListItemAdded(size_t index, AppListItem* item) {
  if (index < kNumFolderTopItems)
    UpdateIcon();
}

void FolderImage::OnListItemRemoved(size_t index, AppListItem* item) {
  if (index < kNumFolderTopItems)
    UpdateIcon();
}

void FolderImage::OnListItemMoved(size_t from_index,
                                  size_t to_index,
                                  AppListItem* item) {
  if (from_index < kNumFolderTopItems || to_index < kNumFolderTopItems)
    UpdateIcon();
}

void FolderImage::RedrawIconAndNotify() {
  FolderImageSource::Icons top_icons;
  for (const auto* item : top_items_)
    top_icons.push_back(item->GetIcon(app_list_config_->type()));
  const gfx::Size icon_size = app_list_config_->folder_unclipped_icon_size();
  icon_ = gfx::ImageSkia(std::make_unique<FolderImageSource>(
                             *app_list_config_, top_icons, icon_size),
                         icon_size);

  for (auto& observer : observers_)
    observer.OnFolderImageUpdated(app_list_config_->type());
}

}  // namespace ash
