/* 
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; version 2 of the
 * License.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include "stdafx.h"
#include "wf_popup.h"
#include "wf_view.h"

#include <cairo/cairo-Win32.h>

using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Threading;

using namespace MySQL;
using namespace MySQL::Forms;
using namespace MySQL::Utilities;
using namespace MySQL::Utilities::SysUtils;

//-------------------------------------------------------------------------------------------------

/**
 * Converts Windows specific mouse button identifiers to plain numbers for the back end.
 */
static int convert_mouse_button(MouseButtons button)
{
  switch (button)
  {
    case MouseButtons::Left:
      return 0;
    case MouseButtons::Right:
      return 1;
    case MouseButtons::Middle:
      return 2;
    default:
      return -1;
  }
}

//----------------- PopupControl ------------------------------------------------------------------

PopupControl::PopupControl()
{
  AutoScaleMode = Windows::Forms::AutoScaleMode::Font;
  AutoValidate = Windows::Forms::AutoValidate::Disable;
  FormBorderStyle = Windows::Forms::FormBorderStyle::None;
  Name = "PopupControl";
  ShowIcon = false;
  ShowInTaskbar = false;
  StartPosition = FormStartPosition::Manual;
  UseWaitCursor = false;
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseDown(MouseEventArgs ^e)
{
  Form::OnMouseDown(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_down(convert_mouse_button(e->Button), e->X, e->Y);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseUp(MouseEventArgs ^e)
{
  Form::OnMouseUp(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_up(convert_mouse_button(e->Button), e->X, e->Y);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseClick(MouseEventArgs ^e)
{
  Form::OnMouseClick(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_click(convert_mouse_button(e->Button), e->X, e->Y);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseDoubleClick(MouseEventArgs ^e)
{
  Form::OnMouseDoubleClick(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_double_click(convert_mouse_button(e->Button), e->X, e->Y);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseMove(MouseEventArgs ^e)
{
  Form::OnMouseMove(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_move(e->X, e->Y);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseLeave(EventArgs ^e)
{
  Form::OnMouseLeave(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL && !popup->is_destroying())
    popup->mouse_leave();
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnMouseEnter(EventArgs ^e)
{
  Form::OnMouseEnter(e);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->mouse_enter();
}

//-------------------------------------------------------------------------------------------------

void PopupControl::OnKeyPress(KeyPressEventArgs^ e)
{
  if (e->KeyChar == 27) // Escape char
    Close();

  Form::OnKeyPress(e);
}

//-------------------------------------------------------------------------------------------------

void PopupControl::DoRepaint()
{
  Invalidate();
}

//-------------------------------------------------------------------------------------------------

/**
 * Shows the popup with its hotspot at the given position. The window is moved accordingly and also
 * considers screen borders.
 */
int PopupControl::Show(int x, int y)
{
  // In the base class the hot spot is the upper left corner.
  Point newLocation= Point(x, y);
  Screen^ activeScreen= Screen::FromControl(Form::ActiveForm);
  System::Drawing::Rectangle screenBounds= activeScreen->Bounds;
  
  if (newLocation.X < screenBounds.Left)
    newLocation.X= screenBounds.Left;
  if (newLocation.X + Width > screenBounds.Right)
    newLocation.X= screenBounds.Right - Width;

  if (newLocation.Y < screenBounds.Top)
    newLocation.Y= screenBounds.Top;
  if (newLocation.Y + Height > screenBounds.Bottom)
    newLocation.Y= screenBounds.Bottom - Height;

  Location = newLocation;
  return (int) ShowDialog();
}

//----------------- StandardPopupControl ----------------------------------------------------------

StandardPopupControl::StandardPopupControl()
: PopupControl()
{
}

//-------------------------------------------------------------------------------------------------

void StandardPopupControl::OnPaint(PaintEventArgs ^e)
{
  IntPtr hdc= e->Graphics->GetHdc();

  cairo_surface_t* surface= cairo_win32_surface_create(static_cast<HDC>(hdc.ToPointer()));
  cairo_t* cr= cairo_create(surface);

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->repaint(cr, e->ClipRectangle.X, e->ClipRectangle.Y, e->ClipRectangle.Width, e->ClipRectangle.Height);

  cairo_destroy(cr);
  cairo_surface_destroy(surface);

  e->Graphics->ReleaseHdc(hdc);
}

//----------------- TransparentPopupControl --------------------------------------------------------

TransparentPopupControl::TransparentPopupControl()
  : PopupControl()
{
  BackColor = Color::Black;
  ForeColor = Color::White;
  Name = "TransparentPopupControl";
  Padding = Windows::Forms::Padding(26, 14, 26, 14);
  borderSize = 3;
  shadowSize = 20;
  shadowColor = Color::Black;
  shadowOffset = 3;
  cornerSize = 20;
  animationSteps = 8;
}

//-------------------------------------------------------------------------------------------------

/**
 * Runs a local message loop to simulate a modal form. Due to way the popup is displayed we cannot
 * use ShowDialog.
 */
void TransparentPopupControl::RunLoop()
{
  modalResult= -1;

  while (modalResult < 0) // TODO: maybe we need to listen to the application exit event.
  {
    Application::DoEvents();
    Thread::Sleep(20);
  }
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::DoRepaint()
{
  if (Visible)
  {
    PrepareBitmap();
    ControlUtilities::SetBitmap(this, contentBitmap, 255);
  }
}

//-------------------------------------------------------------------------------------------------

int TransparentPopupControl::Show(int x, int y)
{
  // Release the current mouse capture, in case this method was called in a mouse down/click event handler.
  Win32::ReleaseCapture();

  hotSpot.X= x;
  hotSpot.Y= y;

  baseSize = System::Drawing::Size(825, 351); // Size by design, without outer shadow.
  Size = System::Drawing::Size(baseSize.Width + 2 * shadowSize + shadowOffset, baseSize.Height + 2 * shadowSize + shadowOffset);

  // The given position is a hot spot, i.e. we have to move the popup so that our hot spot is at that
  // location. For now we define a point on the border in the right upper border as hotspot.
  // This will later be extended to include an arrow or pointer tip.
  Point newLocation= Point(x - baseSize.Width - shadowSize + shadowOffset + cornerSize, y - shadowSize + shadowOffset);
  Screen^ currentScreen= Screen::FromControl(this);
  System::Drawing::Rectangle screenBounds= currentScreen->WorkingArea;

  if (newLocation.X < screenBounds.Left)
    newLocation.X= screenBounds.Left;
  if (newLocation.X + Width > screenBounds.Right)
    newLocation.X= screenBounds.Right - Width;

  if (newLocation.Y < screenBounds.Top)
    newLocation.Y= screenBounds.Top;
  if (newLocation.Y + Height > screenBounds.Bottom)
    newLocation.Y= screenBounds.Bottom - Height;

  Location= newLocation;

  UpdateAndShowPopup(true);
  Focus();
  RunLoop();
  HidePopup();

  return modalResult;
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::UpdateAndShowPopup(bool doAnimated)
{
  PrepareBitmap();

  // Don't use animations in a terminal session (remote desktop).
  animated= doAnimated && !SystemInformation::TerminalServerSession;
  if (animated)
  {
    ControlUtilities::SetBitmap(this, contentBitmap, 0);
    Win32::ShowWindow(Handle, (unsigned int) SW::SHOW);
    for (int i = 1; i <= animationSteps; i++)
      ControlUtilities::SetBitmap(this, contentBitmap, (int)(255.0 * i / animationSteps));
  }
  else
  {
    ControlUtilities::SetBitmap(this, contentBitmap, 255);
    Win32::ShowWindow(Handle, (unsigned int) SW::SHOW);
  }
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::HidePopup()
{
  if (animated && !IsDisposed)
  {
    for (int i = animationSteps; i >= 0;  i--)
      ControlUtilities::SetBitmap(this, contentBitmap, (int)(255.0 * i / animationSteps));
  }
  Close();
}

//-------------------------------------------------------------------------------------------------

GraphicsPath^ TransparentPopupControl::GetPath()
{
  // Generate the outline of the actual content area. Center it around the origin.
  // It will later get transformed to the final position.
  GraphicsPath^ result = gcnew GraphicsPath();

  float width = (float) baseSize.Width;
  float height = (float) baseSize.Height;
  float cornerSizeF = (float) cornerSize; // Just to avoid a dozen type casts.
  System::Drawing::RectangleF bounds = System::Drawing::RectangleF(- width / 2, -height / 2, width, height);
  result->AddArc(bounds.Left, bounds.Top, cornerSizeF, cornerSizeF, 180, 90);
  result->AddArc(bounds.Right - cornerSizeF, bounds.Top, cornerSizeF, cornerSizeF, -90, 90);
  result->AddArc(bounds.Right - cornerSizeF, bounds.Bottom - cornerSizeF, cornerSizeF, cornerSizeF, 0, 90);
  result->AddArc(bounds.Left, bounds.Bottom - cornerSizeF, cornerSizeF, cornerSizeF, 90, 90);
  result->CloseAllFigures();
  return result;
}

//-------------------------------------------------------------------------------------------------

/// <summary>
/// Prepares the bitmap used to draw the window. Layered windows (like this one) use a bitmap for their
/// content, including alpha channel.
/// </summary>
void TransparentPopupControl::PrepareBitmap()
{
  contentBitmap = gcnew Bitmap(Width, Height);

  GraphicsPath^ path = GetPath();
  GraphicsPath^ innerPath = (GraphicsPath^) path->Clone();

  // Increase size of the outline by the shadow size and move it to the center.
  // The inner path keeps the original bounds for clipping.
  Matrix^ matrix = gcnew Matrix();

  float offsetX = (float) Width / 2;
  float offsetY = (float) Height / 2;
  matrix->Translate(offsetX + shadowOffset, offsetY + shadowOffset);
  matrix->Scale(1 + (2 * shadowSize + borderSize) / (float) baseSize.Width,
    1 + (2 * shadowSize + borderSize) / (float) baseSize.Height);
  path->Transform(matrix);

  // Also move the inner part to its final place.
  matrix->Reset();
  matrix->Translate(offsetX, offsetY);
  innerPath->Transform(matrix);

  Graphics^ g = Graphics::FromImage(contentBitmap);
  g->SmoothingMode = SmoothingMode::HighQuality;
  
  // Fill interior.
  Brush^ brush = gcnew SolidBrush(Color::FromArgb(191, 1, 0, 0));
  g->FillPath(brush, innerPath);
  delete brush;

  // ... and draw border around the interior.
  Pen^ borderPen = gcnew Pen(Color::FromArgb(200, Color::White));
  borderPen->EndCap = LineCap::Round;
  borderPen->StartCap  = LineCap::Round;
  borderPen->Width = borderSize;
  GraphicsPath^ borderPath = (GraphicsPath^) innerPath->Clone();
  borderPath->Widen(borderPen);

  brush= gcnew SolidBrush(Color::FromArgb(255, Color::White));
  g->FillPath(brush, borderPath);
  delete brush;

  // Clip out interior. Exclude both, the panel itself as well as its border.
  System::Drawing::Region^ region = gcnew System::Drawing::Region(innerPath);
  g->SetClip(region, CombineMode::Exclude);
  delete region;

  innerPath->Widen(borderPen);
  region = gcnew System::Drawing::Region(innerPath);
  g->SetClip(region, CombineMode::Exclude);
  delete region;

  PathGradientBrush^ backStyle = gcnew PathGradientBrush(path);

  backStyle->CenterColor = shadowColor;
  array<Color>^ colors= { Color::Transparent };
  backStyle->SurroundColors = colors;

  // Make a smooth fade out of the shadow color using the built-in sigma bell curve generator.
  backStyle->SetSigmaBellShape((float) 0.4, 1);

  // Now draw the shadow.
  g->FillPath(backStyle, path);
  delete backStyle;

  // Remove clipping for the remaining interior.
  g->ResetClip();
  RectangleF innerBounds = innerPath->GetBounds();

  // Create a 32 bit image surface for cairo from our bitmap.
  Imaging::BitmapData^ bitmapData= contentBitmap->LockBits(System::Drawing::Rectangle(0, 0, Width, Height),
    Imaging::ImageLockMode::ReadWrite, contentBitmap->PixelFormat);
  unsigned char* data= (unsigned char*) bitmapData->Scan0.ToPointer();
  cairo_surface_t* surface= cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, Width, Height, bitmapData->Stride);

  // Let the backend draw its part.
  cairo_t* cr= cairo_create(surface);
  MySQL::Geometry::Rect displayRect= DisplayRect;

  mforms::Popup* popup= ObjectImpl::get_backend_control<mforms::Popup>(this);
  if (popup != NULL)
    popup->repaint(cr, (int) displayRect.left(), (int) displayRect.top(), (int) displayRect.width(),
      (int) displayRect.height());

  contentBitmap->UnlockBits(bitmapData);
  cairo_destroy(cr);
  cairo_surface_destroy(surface);
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::OnKeyPress(KeyPressEventArgs^ e)
{
  if (e->KeyChar == 27) // Escape char
    modalResult= 0;
  else
    PopupControl::OnKeyPress(e);
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::OnLostFocus(EventArgs^ e)
{
  // When we lose the focus then it means the same as pressing escape. The popup closes with a 
  // cancel result.
  modalResult= 0;
  PopupControl::OnLostFocus(e);
}

//-------------------------------------------------------------------------------------------------

void TransparentPopupControl::OnMouseDown(MouseEventArgs^ e)
{
  if (!ClientRectangle.Contains(e->Location))
    modalResult= 0;
  else
    PopupControl::OnMouseDown(e);
}

//----------------- PopupImpl -------------------------------------------------------------------

PopupImpl::PopupImpl(::mforms::Popup *self)
  : ObjectImpl(self)
{
}


//-------------------------------------------------------------------------------------------------

PopupImpl::~PopupImpl()
{
}

//-------------------------------------------------------------------------------------------------

bool PopupImpl::create(::mforms::Popup *self, mforms::PopupStyle style)
{
  PopupImpl^ popup= gcnew PopupImpl(self);

  if (popup != nullptr)
  {
    PopupControl^ control;
    switch (style)
    {
      case mforms::PopupPlain:
        control= ObjectImpl::create<StandardPopupControl>(self, popup);
        break;
      case mforms::PopupBezel:
        control= ObjectImpl::create<TransparentPopupControl>(self, popup);
        break;
      default:
        control= ObjectImpl::create<PopupControl>(self, popup);
    }

    return true;
  }
  return false;
}

//-------------------------------------------------------------------------------------------------

void PopupImpl::set_needs_repaint(::mforms::Popup *self)
{
  PopupImpl^ popup= (PopupImpl^)ObjectImpl::FromUnmanaged(self);

  PopupControl^ control= popup->get_control<PopupControl>();
  control->DoRepaint();
}

//-------------------------------------------------------------------------------------------------

void PopupImpl::set_size(mforms::Popup *self, int width, int height)
{
  PopupImpl^ popup= (PopupImpl^)ObjectImpl::FromUnmanaged(self);

  PopupControl^ control= popup->get_control<PopupControl>();
  control->Size = Size(width, height);
}

//-------------------------------------------------------------------------------------------------

int PopupImpl::show(mforms::Popup *self, int spot_x, int spot_y)
{
  PopupImpl^ popup= (PopupImpl^)ObjectImpl::FromUnmanaged(self);

  PopupControl^ control= popup->get_control<PopupControl>();
  control->DoRepaint();
  control->Show(spot_x, spot_y);

  return 0;
}

//-------------------------------------------------------------------------------------------------

MySQL::Geometry::Rect PopupImpl::get_content_rect(mforms::Popup *self)
{
  PopupImpl^ popup= (PopupImpl^)ObjectImpl::FromUnmanaged(self);

  PopupControl^ control= popup->get_control<PopupControl>();
  return control->DisplayRect;
}

//-------------------------------------------------------------------------------------------------

void PopupImpl::set_modal_result(mforms::Popup *self, int result)
{
  PopupImpl^ popup= (PopupImpl^)ObjectImpl::FromUnmanaged(self);
  PopupControl^ control= popup->get_control<PopupControl>();
  control->ModalResult= result;
}

//-------------------------------------------------------------------------------------------------

