/* 
 * Copyright (c) 2010, 2014, 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 "wf_base.h"
#include "wf_view.h"
#include "wf_popup.h"

using namespace System::Drawing;
using namespace System::Drawing::Drawing2D;
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 mforms::MouseButton convert_mouse_button(MouseButtons button)
{
  switch (button)
  {
    case MouseButtons::Left:
      return mforms::MouseButtonLeft;
    case MouseButtons::Right:
      return mforms::MouseButtonRight;
    case MouseButtons::Middle:
      return mforms::MouseButtonOther;
    default:
      return mforms::MouseButtonNone;
  }
}

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

ref class MySQL::Forms::PopupControl : Windows::Forms::Form
{
protected:
  int modalResult;

public:
  mforms::Popup *backend;

  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;
  }

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

  virtual void OnMouseDown(MouseEventArgs ^args) override
  {
    __super::OnMouseDown(args);
    backend->mouse_down(convert_mouse_button(args->Button), args->X, args->Y);
  }

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

  virtual void OnMouseUp(MouseEventArgs ^args) override
  {
    __super::OnMouseUp(args);
    backend->mouse_up(convert_mouse_button(args->Button), args->X, args->Y);
  }

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

  virtual void OnMouseClick(MouseEventArgs ^args) override
  {
    __super::OnMouseClick(args);
    backend->mouse_click(convert_mouse_button(args->Button), args->X, args->Y);
  }

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

  virtual void OnMouseDoubleClick(MouseEventArgs ^args) override
  {
    __super::OnMouseDoubleClick(args);
    backend->mouse_double_click(convert_mouse_button(args->Button), args->X, args->Y);
  }

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

  virtual void OnMouseMove(MouseEventArgs ^args) override
  {
    __super::OnMouseMove(args);
    backend->mouse_move(convert_mouse_button(args->Button), args->X, args->Y);
  }

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

  virtual void OnMouseLeave(EventArgs ^args) override
  {
    __super::OnMouseLeave(args);
    backend->mouse_leave();
  }

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

  virtual void OnMouseEnter(EventArgs ^args) override
  {
    __super::OnMouseEnter(args);
    backend->mouse_enter();
  }

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

  virtual void DoRepaint()
  {
    Invalidate();
  }

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

  /**
   * Shows the popup with its hotspot at the given position. The window is moved accordingly and also
   * considers screen borders.
   */
  virtual 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();
  }

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

  virtual property base::Rect DisplayRect
  {
    base::Rect get()
    {
      System::Drawing::Rectangle content_area = ClientRectangle;
      content_area.X += Padding.Left;
      content_area.Y += Padding.Top;
      content_area.Width -= Padding.Horizontal;
      content_area.Height -= Padding.Vertical;

      return base::Rect(content_area.Left, content_area.Top, content_area.Width, content_area.Height);
    }
  }

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

  virtual property int ModalResult
  {
    int get() { return modalResult; }
    void set(int value) { modalResult = value; }
  }

};

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

/**
  * A simple transparent and rectangular window.
  */
ref class TransparentPopupControl: PopupControl
{
private:
  Bitmap ^contentBitmap;

public:
  TransparentPopupControl::TransparentPopupControl()
    : PopupControl()
  {
    contentBitmap = nullptr;
  }

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


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

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

  /// <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 PrepareBitmap()
  {
    contentBitmap = gcnew Bitmap(Width, Height);
    Graphics^ g = Graphics::FromImage(contentBitmap);
    g->SmoothingMode = SmoothingMode::HighQuality;

    // 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);
    base::Rect displayRect = DisplayRect;

    backend->repaint(cr, (int) displayRect.left(), (int) displayRect.top(),
                        (int) displayRect.width(), (int) displayRect.height());

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

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

  virtual void OnDeactivate(EventArgs ^args) override
  {
    __super::OnDeactivate(args);

    Close();
    backend->closed();
  }

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

  virtual void OnKeyPress(KeyPressEventArgs ^args) override
  {
    if (args->KeyChar == 27) // Escape char
      Close(); // Will trigger OnDeactivate.
    else
      __super::OnKeyPress(args);
  }

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

  virtual int Show(int x, int y) override
  {
    Point newLocation = Point(x, y);

    Location = newLocation;
    TopMost = true;
    PrepareBitmap();
    ControlUtilities::SetBitmap(this, contentBitmap, 255);
    Win32::ShowWindow(Handle, (unsigned int) SW::SHOWNOACTIVATE);
    Capture = true;

    return 1;
  }

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

  virtual property int ModalResult
  {
    void set(int value) override
    {
      // Setting a modal result is our sign to close.
      modalResult = value;
      Close();
    }
  }

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

  virtual property Windows::Forms::CreateParams^ CreateParams
  {
    Windows::Forms::CreateParams^ get() override
    {
      Windows::Forms::CreateParams^ cp = PopupControl::CreateParams;
      cp->ExStyle |= WS_EX_LAYERED | WS_EX_NOACTIVATE;
      return cp;
    }
  }

};

//----------------- BezelPopupControl --------------------------------------------------------------

ref class BezelPopupControl: PopupControl
{
private:
  Bitmap ^contentBitmap;
  Drawing::Size baseSize;
  float borderSize;
  int shadowSize;
  Drawing::Color shadowColor;
  int shadowOffset;
  int cornerSize;
  int animationSteps;
  bool animated;
  Drawing::Point hotSpot;

public:
  BezelPopupControl()
    : PopupControl()
  {
    BackColor = Color::Black;
    ForeColor = Color::White;
    Name = "BezelPopupControl";
    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 the way the popup is displayed we
   * cannot use ShowDialog.
   */
  void RunLoop()
  {
    modalResult = -1;

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

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

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

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

  virtual int Show(int x, int y) override
  {
    // 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 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 HidePopup()
  {
    if (animated && !IsDisposed)
    {
      for (int i = animationSteps; i >= 0;  i--)
        ControlUtilities::SetBitmap(this, contentBitmap, (int)(255.0 * i / animationSteps));
    }
    Close();
  }

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

  GraphicsPath^ 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 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);
    base::Rect displayRect = DisplayRect;

    backend->repaint(cr, (int) displayRect.left(), (int) displayRect.top(), (int) displayRect.width(),
      (int) displayRect.height());

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

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

  virtual void OnKeyPress(KeyPressEventArgs ^args) override
  {
    if (args->KeyChar == 27) // Escape char
      modalResult= 0;
    else
      __super::OnKeyPress(args);
  }

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

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

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

  virtual void OnMouseDown(MouseEventArgs ^args) override
  {
    if (!ClientRectangle.Contains(args->Location))
      modalResult = 0;
    else
      __super::OnMouseDown(args);
  }

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

  virtual property base::Rect DisplayRect
  {
    base::Rect get() override
    {
      base::Rect rect(0, 0, baseSize.Width, baseSize.Height);
      rect.pos.x += shadowSize + shadowOffset + Padding.Left;
      rect.pos.y += shadowSize + shadowOffset + Padding.Top;
      rect.size.width -= Padding.Horizontal;
      rect.size.height -= Padding.Vertical;

      return rect;
    }
  }

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

  virtual property bool ShowWithoutActivation
  {
    bool get() override { return true; }
  }

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

  virtual property Windows::Forms::CreateParams^ CreateParams
  {
    Windows::Forms::CreateParams^ get() override
    {
      Windows::Forms::CreateParams^ cp = __super::CreateParams;

      cp->ExStyle |= (int) MySQL::Utilities::SysUtils::WS::EX_LAYERED;
      cp->ExStyle |= (int) MySQL::Utilities::SysUtils::WS::EX_NOACTIVATE;
      return cp;
    }
  }

};

//----------------- PopupWrapper -------------------------------------------------------------------

PopupWrapper::PopupWrapper(mforms::Popup *backend)
  : ObjectWrapper(backend)
{
}


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

bool PopupWrapper::create(mforms::Popup *backend, mforms::PopupStyle style)
{
  PopupWrapper *wrapper = new PopupWrapper(backend);

  PopupControl ^popup = nullptr;
  switch (style)
  {
    case mforms::PopupPlain:
      popup = PopupWrapper::Create<TransparentPopupControl>(backend, wrapper);
      break;

    case mforms::PopupBezel:
      popup = PopupWrapper::Create<BezelPopupControl>(backend, wrapper);
      break;

    default:
      popup = PopupWrapper::Create<PopupControl>(backend, wrapper);
      break;
  }

  if (popup != nullptr)
    popup->backend = backend;

  return true;
}

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

void PopupWrapper::destroy(mforms::Popup *backend)
{
}

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

void PopupWrapper::set_needs_repaint(mforms::Popup *backend)
{
  PopupControl ^control = PopupWrapper::GetManagedObject<PopupControl>(backend);
  control->DoRepaint();
}

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

void PopupWrapper::set_size(mforms::Popup *backend, int width, int height)
{
  PopupControl ^control = PopupWrapper::GetManagedObject<PopupControl>(backend);
  control->Size = Size(width, height);
}

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

int PopupWrapper::show(mforms::Popup *backend, int spot_x, int spot_y)
{
  PopupControl ^control = PopupWrapper::GetManagedObject<PopupControl>(backend);
  control->DoRepaint();
  control->Show(spot_x, spot_y);

  return 0;
}

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

base::Rect PopupWrapper::get_content_rect(mforms::Popup *backend)
{
  PopupControl ^control = PopupWrapper::GetManagedObject<PopupControl>(backend);
  return control->DisplayRect;
}

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

void PopupWrapper::set_modal_result(mforms::Popup *backend, int result)
{
  PopupControl ^control = PopupWrapper::GetManagedObject<PopupControl>(backend);
  control->ModalResult = result;
}

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

void PopupWrapper::init()
{
  mforms::ControlFactory *f = mforms::ControlFactory::get_instance();

  f->_popup_impl.create = &PopupWrapper::create;
  f->_popup_impl.destroy = &PopupWrapper::destroy;
  f->_popup_impl.set_needs_repaint = &PopupWrapper::set_needs_repaint;
  f->_popup_impl.set_size = &PopupWrapper::set_size;
  f->_popup_impl.show = &PopupWrapper::show;
  f->_popup_impl.get_content_rect = &PopupWrapper::get_content_rect;
  f->_popup_impl.set_modal_result = &PopupWrapper::set_modal_result;
}

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