// Copyright (c) 2006-2008 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 "skia/ext/vector_device.h"

#include "base/gfx/gdi_util.h"
#include "base/gfx/skia_utils.h"
#include "base/logging.h"
#include "base/scoped_handle.h"

#include "SkUtils.h"

namespace gfx {

VectorDevice* VectorDevice::create(HDC dc, int width, int height) {
  InitializeDC(dc);

  // Link the SkBitmap to the current selected bitmap in the device context.
  SkBitmap bitmap;
  HGDIOBJ selected_bitmap = GetCurrentObject(dc, OBJ_BITMAP);
  bool succeeded = false;
  if (selected_bitmap != NULL) {
    BITMAP bitmap_data;
    if (GetObject(selected_bitmap, sizeof(BITMAP), &bitmap_data) ==
        sizeof(BITMAP)) {
      // The context has a bitmap attached. Attach our SkBitmap to it.
      // Warning: If the bitmap gets unselected from the HDC, VectorDevice has
      // no way to detect this, so the HBITMAP could be released while SkBitmap
      // still has a reference to it. Be cautious.
      if (width == bitmap_data.bmWidth &&
          height == bitmap_data.bmHeight) {
        bitmap.setConfig(SkBitmap::kARGB_8888_Config,
                         bitmap_data.bmWidth,
                         bitmap_data.bmHeight,
                         bitmap_data.bmWidthBytes);
        bitmap.setPixels(bitmap_data.bmBits);
        succeeded = true;
      }
    }
  }

  if (!succeeded)
    bitmap.setConfig(SkBitmap::kARGB_8888_Config, width, height);

  return new VectorDevice(dc, bitmap);
}

VectorDevice::VectorDevice(HDC dc, const SkBitmap& bitmap)
    : PlatformDeviceWin(bitmap),
      hdc_(dc),
      previous_brush_(NULL),
      previous_pen_(NULL) {
  transform_.reset();
}

VectorDevice::~VectorDevice() {
  DCHECK(previous_brush_ == NULL);
  DCHECK(previous_pen_ == NULL);
}


void VectorDevice::drawPaint(const SkDraw& draw, const SkPaint& paint) {
  // TODO(maruel):  Bypass the current transformation matrix.
  SkRect rect;
  rect.fLeft = 0;
  rect.fTop = 0;
  rect.fRight = SkIntToScalar(width() + 1);
  rect.fBottom = SkIntToScalar(height() + 1);
  drawRect(draw, rect, paint);
}

void VectorDevice::drawPoints(const SkDraw& draw, SkCanvas::PointMode mode,
                              size_t count, const SkPoint pts[],
                              const SkPaint& paint) {
  if (!count)
    return;

  if (mode == SkCanvas::kPoints_PointMode) {
    NOTREACHED();
    return;
  }

  SkPaint tmp_paint(paint);
  tmp_paint.setStyle(SkPaint::kStroke_Style);

  // Draw a path instead.
  SkPath path;
  switch (mode) {
    case SkCanvas::kLines_PointMode:
      if (count % 2) {
        NOTREACHED();
        return;
      }
      for (size_t i = 0; i < count / 2; ++i) {
        path.moveTo(pts[2 * i]);
        path.lineTo(pts[2 * i + 1]);
      }
      break;
    case SkCanvas::kPolygon_PointMode:
      path.moveTo(pts[0]);
      for (size_t i = 1; i < count; ++i) {
        path.lineTo(pts[i]);
      }
      break;
    default:
      NOTREACHED();
      return;
  }
  // Draw the calculated path.
  drawPath(draw, path, tmp_paint);
}

void VectorDevice::drawRect(const SkDraw& draw, const SkRect& rect,
                            const SkPaint& paint) {
  if (paint.getPathEffect()) {
    // Draw a path instead.
    SkPath path_orginal;
    path_orginal.addRect(rect);

    // Apply the path effect to the rect.
    SkPath path_modified;
    paint.getFillPath(path_orginal, &path_modified);

    // Removes the path effect from the temporary SkPaint object.
    SkPaint paint_no_effet(paint);
    paint_no_effet.setPathEffect(NULL)->safeUnref();

    // Draw the calculated path.
    drawPath(draw, path_modified, paint_no_effet);
    return;
  }

  if (!ApplyPaint(paint)) {
    return;
  }
  HDC dc = getBitmapDC();
  if (!Rectangle(dc, SkScalarRound(rect.fLeft),
                 SkScalarRound(rect.fTop),
                 SkScalarRound(rect.fRight),
                 SkScalarRound(rect.fBottom))) {
    NOTREACHED();
  }
  Cleanup();
}

void VectorDevice::drawPath(const SkDraw& draw, const SkPath& path,
                            const SkPaint& paint) {
  if (paint.getPathEffect()) {
    // Apply the path effect forehand.
    SkPath path_modified;
    paint.getFillPath(path, &path_modified);

    // Removes the path effect from the temporary SkPaint object.
    SkPaint paint_no_effet(paint);
    paint_no_effet.setPathEffect(NULL)->safeUnref();

    // Draw the calculated path.
    drawPath(draw, path_modified, paint_no_effet);
    return;
  }

  if (!ApplyPaint(paint)) {
    return;
  }
  HDC dc = getBitmapDC();
  PlatformDeviceWin::LoadPathToDC(dc, path);
  switch (paint.getStyle()) {
    case SkPaint::kFill_Style: {
      BOOL res = StrokeAndFillPath(dc);
      DCHECK(res != 0);
      break;
    }
    case SkPaint::kStroke_Style: {
      BOOL res = StrokePath(dc);
      DCHECK(res != 0);
      break;
    }
    case SkPaint::kStrokeAndFill_Style: {
      BOOL res = StrokeAndFillPath(dc);
      DCHECK(res != 0);
      break;
    }
    default:
      NOTREACHED();
      break;
  }
  Cleanup();
}

void VectorDevice::drawBitmap(const SkDraw& draw, const SkBitmap& bitmap,
                              const SkMatrix& matrix, const SkPaint& paint) {
  // Load the temporary matrix. This is what will translate, rotate and resize
  // the bitmap.
  SkMatrix actual_transform(transform_);
  actual_transform.preConcat(matrix);
  LoadTransformToDC(hdc_, actual_transform);

  InternalDrawBitmap(bitmap, 0, 0, paint);

  // Restore the original matrix.
  LoadTransformToDC(hdc_, transform_);
}

void VectorDevice::drawSprite(const SkDraw& draw, const SkBitmap& bitmap,
                              int x, int y, const SkPaint& paint) {
  SkMatrix identity;
  identity.reset();
  LoadTransformToDC(hdc_, identity);

  InternalDrawBitmap(bitmap, x, y, paint);

  // Restore the original matrix.
  LoadTransformToDC(hdc_, transform_);
}

void VectorDevice::drawText(const SkDraw& draw, const void* text, size_t byteLength,
                            SkScalar x, SkScalar y, const SkPaint& paint) {
  // This function isn't used in the code. Verify this assumption.
  NOTREACHED();
}

void VectorDevice::drawPosText(const SkDraw& draw, const void* text, size_t len,
                               const SkScalar pos[], SkScalar constY,
                               int scalarsPerPos, const SkPaint& paint) {
  // This function isn't used in the code. Verify this assumption.
  NOTREACHED();
}

void VectorDevice::drawTextOnPath(const SkDraw& draw, const void* text,
                                  size_t len,
                                  const SkPath& path, const SkMatrix* matrix,
                                  const SkPaint& paint) {
  // This function isn't used in the code. Verify this assumption.
  NOTREACHED();
}

void VectorDevice::drawVertices(const SkDraw& draw, SkCanvas::VertexMode vmode,
                                int vertexCount,
                                const SkPoint vertices[], const SkPoint texs[],
                                const SkColor colors[], SkXfermode* xmode,
                                const uint16_t indices[], int indexCount,
                                const SkPaint& paint) {
  // This function isn't used in the code. Verify this assumption.
  NOTREACHED();
}

void VectorDevice::drawDevice(const SkDraw& draw, SkDevice* device, int x,
                              int y, const SkPaint& paint) {
  // TODO(maruel):  http://b/1183870 Playback the EMF buffer at printer's dpi if
  // it is a vectorial device.
  drawSprite(draw, device->accessBitmap(false), x, y, paint);
}

bool VectorDevice::ApplyPaint(const SkPaint& paint) {
  // Note: The goal here is to transfert the SkPaint's state to the HDC's state.
  // This function does not execute the SkPaint drawing commands. These should
  // be executed in drawPaint().

  SkPaint::Style style = paint.getStyle();
  if (!paint.getAlpha())
    style = SkPaint::kStyleCount;

  switch (style) {
    case SkPaint::kFill_Style:
      if (!CreateBrush(true, paint) ||
          !CreatePen(false, paint))
        return false;
      break;
    case SkPaint::kStroke_Style:
      if (!CreateBrush(false, paint) ||
          !CreatePen(true, paint))
        return false;
      break;
    case SkPaint::kStrokeAndFill_Style:
      if (!CreateBrush(true, paint) ||
          !CreatePen(true, paint))
        return false;
      break;
    default:
      if (!CreateBrush(false, paint) ||
          !CreatePen(false, paint))
        return false;
      break;
  }

  /*
  getFlags();
    isAntiAlias();
    isDither()
    isLinearText()
    isSubpixelText()
    isUnderlineText()
    isStrikeThruText()
    isFakeBoldText()
    isDevKernText()
    isFilterBitmap()

  // Skia's text is not used. This should be fixed.
  getTextAlign()
  getTextScaleX()
  getTextSkewX()
  getTextEncoding()
  getFontMetrics()
  getFontSpacing()
  */

  // BUG 1094907: Implement shaders. Shaders currently in use:
  //  SkShader::CreateBitmapShader
  //  SkGradientShader::CreateRadial
  //  SkGradientShader::CreateLinear
  // DCHECK(!paint.getShader());

  // http://b/1106647 Implement loopers and mask filter. Looper currently in
  // use:
  //   SkBlurDrawLooper is used for shadows.
  // DCHECK(!paint.getLooper());
  // DCHECK(!paint.getMaskFilter());

  // http://b/1165900 Implement xfermode.
  // DCHECK(!paint.getXfermode());

  // The path effect should be processed before arriving here.
  DCHECK(!paint.getPathEffect());

  // These aren't used in the code. Verify this assumption.
  DCHECK(!paint.getColorFilter());
  DCHECK(!paint.getRasterizer());
  // Reuse code to load Win32 Fonts.
  DCHECK(!paint.getTypeface());
  return true;
}

void VectorDevice::setMatrixClip(const SkMatrix& transform,
                                 const SkRegion& region) {
  transform_ = transform;
  LoadTransformToDC(hdc_, transform_);
  clip_region_ = region;
  if (!clip_region_.isEmpty())
    LoadClipRegion();
}

void VectorDevice::drawToHDC(HDC dc, int x, int y, const RECT* src_rect) {
  NOTREACHED();
}

void VectorDevice::LoadClipRegion() {
  SkMatrix t;
  t.reset();
  LoadClippingRegionToDC(hdc_, clip_region_, t);
}

bool VectorDevice::CreateBrush(bool use_brush, COLORREF color) {
  DCHECK(previous_brush_ == NULL);
  // We can't use SetDCBrushColor() or DC_BRUSH when drawing to a EMF buffer.
  // SetDCBrushColor() calls are not recorded at all and DC_BRUSH will use
  // WHITE_BRUSH instead.

  if (!use_brush) {
    // Set the transparency.
    if (0 == SetBkMode(hdc_, TRANSPARENT)) {
      NOTREACHED();
      return false;
    }

    // Select the NULL brush.
    previous_brush_ = SelectObject(GetStockObject(NULL_BRUSH));
    return previous_brush_ != NULL;
  }

  // Set the opacity.
  if (0 == SetBkMode(hdc_, OPAQUE)) {
    NOTREACHED();
    return false;
  }

  // Create and select the brush.
  previous_brush_ = SelectObject(CreateSolidBrush(color));
  return previous_brush_ != NULL;
}

bool VectorDevice::CreatePen(bool use_pen, COLORREF color, int stroke_width,
                             float stroke_miter, DWORD pen_style) {
  DCHECK(previous_pen_ == NULL);
  // We can't use SetDCPenColor() or DC_PEN when drawing to a EMF buffer.
  // SetDCPenColor() calls are not recorded at all and DC_PEN will use BLACK_PEN
  // instead.

  // No pen case
  if (!use_pen) {
    previous_pen_ = SelectObject(GetStockObject(NULL_PEN));
    return previous_pen_ != NULL;
  }

  // Use the stock pen if the stroke width is 0.
  if (stroke_width == 0) {
    // Create a pen with the right color.
    previous_pen_ = SelectObject(::CreatePen(PS_SOLID, 0, color));
    return previous_pen_ != NULL;
  }

  // Load a custom pen.
  LOGBRUSH brush;
  brush.lbStyle = BS_SOLID;
  brush.lbColor = color;
  brush.lbHatch = 0;
  HPEN pen = ExtCreatePen(pen_style, stroke_width, &brush, 0, NULL);
  DCHECK(pen != NULL);
  previous_pen_ = SelectObject(pen);
  if (previous_pen_ == NULL)
    return false;

  if (!SetMiterLimit(hdc_, stroke_miter, NULL)) {
    NOTREACHED();
    return false;
  }
  return true;
}

void VectorDevice::Cleanup() {
  if (previous_brush_) {
    HGDIOBJ result = SelectObject(previous_brush_);
    previous_brush_ = NULL;
    if (result) {
      BOOL res = DeleteObject(result);
      DCHECK(res != 0);
    }
  }
  if (previous_pen_) {
    HGDIOBJ result = SelectObject(previous_pen_);
    previous_pen_ = NULL;
    if (result) {
      BOOL res = DeleteObject(result);
      DCHECK(res != 0);
    }
  }
  // Remove any loaded path from the context.
  AbortPath(hdc_);
}

HGDIOBJ VectorDevice::SelectObject(HGDIOBJ object) {
  HGDIOBJ result = ::SelectObject(hdc_, object);
  DCHECK(result != HGDI_ERROR);
  if (result == HGDI_ERROR)
    return NULL;
  return result;
}

bool VectorDevice::CreateBrush(bool use_brush, const SkPaint& paint) {
  // Make sure that for transparent color, no brush is used.
  if (paint.getAlpha() == 0) {
    // Test if it ever happen.
    NOTREACHED();
    use_brush = false;
  }

  return CreateBrush(use_brush, SkColorToCOLORREF(paint.getColor()));
}

bool VectorDevice::CreatePen(bool use_pen, const SkPaint& paint) {
  // Make sure that for transparent color, no pen is used.
  if (paint.getAlpha() == 0) {
    // Test if it ever happen.
    NOTREACHED();
    use_pen = false;
  }

  DWORD pen_style = PS_GEOMETRIC | PS_SOLID;
  switch (paint.getStrokeJoin()) {
    case SkPaint::kMiter_Join:
      // Connects path segments with a sharp join.
      pen_style |= PS_JOIN_MITER;
      break;
    case SkPaint::kRound_Join:
      // Connects path segments with a round join.
      pen_style |= PS_JOIN_ROUND;
      break;
    case SkPaint::kBevel_Join:
      // Connects path segments with a flat bevel join.
      pen_style |= PS_JOIN_BEVEL;
      break;
    default:
      NOTREACHED();
      break;
  }
  switch (paint.getStrokeCap()) {
    case SkPaint::kButt_Cap:
      // Begin/end contours with no extension.
      pen_style |= PS_ENDCAP_FLAT;
      break;
    case SkPaint::kRound_Cap:
      // Begin/end contours with a semi-circle extension.
      pen_style |= PS_ENDCAP_ROUND;
      break;
    case SkPaint::kSquare_Cap:
      // Begin/end contours with a half square extension.
      pen_style |= PS_ENDCAP_SQUARE;
      break;
    default:
      NOTREACHED();
      break;
  }

  return CreatePen(use_pen,
                   SkColorToCOLORREF(paint.getColor()),
                   SkScalarRound(paint.getStrokeWidth()),
                   paint.getStrokeMiter(),
                   pen_style);
}

void VectorDevice::InternalDrawBitmap(const SkBitmap& bitmap, int x, int y,
                                      const SkPaint& paint) {
  uint8 alpha = paint.getAlpha();
  if (alpha == 0)
    return;

  bool is_translucent;
  if (alpha != 255) {
    // ApplyPaint expect an opaque color.
    SkPaint tmp_paint(paint);
    tmp_paint.setAlpha(255);
    if (!ApplyPaint(tmp_paint))
      return;
    is_translucent = true;
  } else {
    if (!ApplyPaint(paint))
      return;
    is_translucent = false;
  }
  int src_size_x = bitmap.width();
  int src_size_y = bitmap.height();
  if (!src_size_x || !src_size_y)
    return;

  // Create a BMP v4 header that we can serialize.
  BITMAPV4HEADER bitmap_header;
  gfx::CreateBitmapV4Header(src_size_x, src_size_y, &bitmap_header);
  HDC dc = getBitmapDC();
  SkAutoLockPixels lock(bitmap);
  DCHECK_EQ(bitmap.getConfig(), SkBitmap::kARGB_8888_Config);
  const uint32_t* pixels = static_cast<const uint32_t*>(bitmap.getPixels());
  if (pixels == NULL) {
    NOTREACHED();
    return;
  }

  if (!is_translucent) {
    int row_length = bitmap.rowBytesAsPixels();
    // There is no quick way to determine if an image is opaque.
    for (int y2 = 0; y2 < src_size_y; ++y2) {
      for (int x2 = 0; x2 < src_size_x; ++x2) {
        if (SkColorGetA(pixels[(y2 * row_length) + x2]) != 255) {
          is_translucent = true;
          y2 = src_size_y;
          break;
        }
      }
    }
  }

  BITMAPINFOHEADER hdr;
  gfx::CreateBitmapHeader(src_size_x, src_size_y, &hdr);
  if (is_translucent) {
    // The image must be loaded as a bitmap inside a device context.
    ScopedHDC bitmap_dc(::CreateCompatibleDC(dc));
    void* bits = NULL;
    ScopedBitmap hbitmap(::CreateDIBSection(
        bitmap_dc, reinterpret_cast<const BITMAPINFO*>(&hdr),
        DIB_RGB_COLORS, &bits, NULL, 0));
    memcpy(bits, pixels, bitmap.getSize());
    DCHECK(hbitmap);
    HGDIOBJ old_bitmap = ::SelectObject(bitmap_dc, hbitmap);
    DeleteObject(old_bitmap);

    // After some analysis of IE7's behavior, this is the thing to do. I was
    // sure IE7 was doing so kind of bitmasking due to the way translucent image
    // where renderered but after some windbg tracing, it is being done by the
    // printer driver after all (mostly HP printers). IE7 always use AlphaBlend
    // for bitmasked images. The trick seems to switch the stretching mode in
    // what the driver expects.
    DWORD previous_mode = GetStretchBltMode(dc);
    BOOL result = SetStretchBltMode(dc, COLORONCOLOR);
    DCHECK(result);
    // Note that this function expect premultiplied colors (!)
    BLENDFUNCTION blend_function = {AC_SRC_OVER, 0, alpha, AC_SRC_ALPHA};
    result = GdiAlphaBlend(dc,
                           x, y,  // Destination origin.
                           src_size_x, src_size_y,  // Destination size.
                           bitmap_dc,
                           0, 0,  // Source origin.
                           src_size_x, src_size_y,  // Source size.
                           blend_function);
    DCHECK(result);
    result = SetStretchBltMode(dc, previous_mode);
    DCHECK(result);
  } else {
    BOOL result = StretchDIBits(dc,
                                x, y,  // Destination origin.
                                src_size_x, src_size_y,
                                0, 0,  // Source origin.
                                src_size_x, src_size_y,  // Source size.
                                pixels,
                                reinterpret_cast<const BITMAPINFO*>(&hdr),
                                DIB_RGB_COLORS,
                                SRCCOPY);
    DCHECK(result);
  }
  Cleanup();
}

}  // namespace gfx