/* 
 * Copyright (c) 2011, 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_popover.h"
#include "wf_utilities.h"

#include <cairo/cairo-Win32.h>

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

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

using namespace mforms;

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

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

//----------------- PopoverControl -----------------------------------------------------------------

PopoverControl::PopoverControl()
{
  AutoScaleMode = Windows::Forms::AutoScaleMode::Font;
  AutoValidate = Windows::Forms::AutoValidate::Disable;
  FormBorderStyle = Windows::Forms::FormBorderStyle::None;
  //TopMost = true;
  Owner = UtilitiesImpl::get_mainform();
  Name = "PopoverControl";
  ShowIcon = false;
  ShowInTaskbar = false;
  StartPosition = FormStartPosition::Manual;
  UseWaitCursor = false;
  Padding = System::Windows::Forms::Padding(7);

  cornerSize = 14;
  animationSteps = 8;
}

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

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

  Form::OnKeyPress(e);
}

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

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

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

void PopoverControl::SetBaseSize(int width, int height)
{
  baseSize = System::Drawing::Size(width, height);
}

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

#define ARROW_SIZE 16 // Number of pixels from arrow base to arrow tip.
#define ARROW_BASE 32 // Number of pixels the base line of the arrow is wide.

/**
 * Shows the popover with its hotspot at the given position. The window is moved accordingly and also
 * considers screen borders.
 */
void PopoverControl::Show(int x, int y, mforms::StartPosition position)
{
  // 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;

  // Compute the coordinates starting with the given position.
  ComputeCoordinatesAndPadding();
  
  if (!Visible || relativePosition != position)
  {
    relativePosition = position;
    UpdateAndShowPopover(true);
  }
}

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

void PopoverControl::ComputeCoordinatesAndPadding()
{
  // The base size is the size of the main part, without arrow.
  System::Drawing::Size actualSize = baseSize;

  // Add the arrow size to either width or height, depending on the proposed relative position.
  if (relativePosition == mforms::Left || relativePosition == mforms::Right)
    actualSize.Width += ARROW_SIZE;
  else
    actualSize.Height += ARROW_SIZE;

  Size = actualSize;

  // The initial position of the arrow is not the center on its side but only 1/3 of side's size
  // for a more appealing look. Additionally, add the arrow's size to the padding on this size to
  // exclude its area from the main content area.
  Point newLocation;
  switch (relativePosition)
  {
  case mforms::Left:
    newLocation.X = hotSpot.X - actualSize.Width;
    newLocation.Y = hotSpot.Y - actualSize.Height / 3;
    Padding = System::Windows::Forms::Padding(7, 7, 7 + ARROW_SIZE, 7);
    break;
  case mforms::Right:
    newLocation.X = hotSpot.X;
    newLocation.Y = hotSpot.Y - actualSize.Height / 3;
    Padding = System::Windows::Forms::Padding(7 + ARROW_SIZE, 7, 7, 7);
    break;
  case mforms::Above:
    newLocation.X = hotSpot.X - actualSize.Width / 3;
    newLocation.Y = hotSpot.Y - actualSize.Height;
    Padding = System::Windows::Forms::Padding(7, 7, 7, 7 + ARROW_SIZE);
    break;
  case mforms::Below:
    newLocation.X = hotSpot.X - actualSize.Width / 3;
    newLocation.Y = hotSpot.Y;
    Padding = System::Windows::Forms::Padding(7, 7 + ARROW_SIZE, 7, 7);
    break;
  }

  Screen^ currentScreen = Screen::FromHandle(IntPtr(GetForegroundWindow()));
  System::Drawing::Rectangle screenBounds = currentScreen->WorkingArea;

  // Check the control's bounds and determine the amount of pixels we have to move it make
  // it fully appear on screen. This will usually not move the hot spot, unless the movement
  // of the control is so much that it would leave the arrow outside its bounds.
  int deltaX = 0;
  int deltaY = 0;
  if (newLocation.X < screenBounds.Left)
    deltaX = screenBounds.Left - newLocation.X;
  if (newLocation.X + Width > screenBounds.Right)
    deltaX = screenBounds.Right - (newLocation.X + Width);

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

  // Now that we have the final location check the arrow again.
  switch (relativePosition)
  {
  case mforms::Left:
  case mforms::Right:
    hotSpot.X += deltaX;
    if ((hotSpot.Y - ARROW_BASE / 2) < (newLocation.Y + cornerSize))
      hotSpot.Y = newLocation.Y + cornerSize + ARROW_BASE / 2;
    if ((hotSpot.Y + ARROW_BASE / 2) > (newLocation.Y + actualSize.Height - cornerSize))
      hotSpot.Y = newLocation.Y + actualSize.Height - cornerSize - ARROW_BASE / 2;
    break;
  case mforms::Above:
  case mforms::Below:
    if ((hotSpot.X - ARROW_BASE / 2) < (newLocation.X + cornerSize))
      hotSpot.X = newLocation.X + cornerSize + ARROW_BASE / 2;
    if ((hotSpot.X + ARROW_BASE / 2) > (newLocation.X + actualSize.Width - cornerSize))
      hotSpot.X = newLocation.X + actualSize.Width - cornerSize - ARROW_BASE / 2;
    hotSpot.Y += deltaY;
    break;
  }

  Location = newLocation;
}

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

void PopoverControl::OnPaint(PaintEventArgs ^e)
{
  // Draw our border. Child controls placed on the popover do their own drawing as usual.
  Pen^ borderPen = gcnew Pen(Color::FromArgb(255, 170, 170, 170));
  borderPen->Width = 1.5;

  e->Graphics->SmoothingMode = SmoothingMode::HighQuality;
  e->Graphics->DrawPath(borderPen, outline);

  GraphicsPath^ helper = (GraphicsPath^) outline->Clone();
  Matrix^ matrix = gcnew Matrix();
  matrix->Translate(-1, -1);
  matrix->Scale((Width + 2) / (float) Width, (Height + 1) / (float) Height);
  helper->Transform(matrix);
  e->Graphics->DrawPath(borderPen, helper);

  delete borderPen;
}

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

void PopoverControl::UpdateAndShowPopover(bool doAnimated)
{
  ComputeOutline();
  System::Drawing::Region^ region = gcnew System::Drawing::Region(outline);
  this->Region = region;

  // Scale down the outline a bit as lines are drawn not including right and bottom coordinates.
  Matrix^ matrix = gcnew Matrix();
  matrix->Scale((Width - 1) / (float) Width, (Height - 0.5f) / (float) Height);
  outline->Transform(matrix);

  // Don't use animations in a terminal session (remote desktop).
  animated= doAnimated && !SystemInformation::TerminalServerSession;
  if (animated)
  {
    Opacity = 0;
    Visible = true;
    Update();
    for (int i = 1; i <= animationSteps; i++)
    {
      Opacity = i / (float) animationSteps;
      Sleep(200 / animationSteps); // 200ms for the entire fade.
    }
  }
  else
    Show();
}

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

void PopoverControl::HidePopup()
{
  if (animated && !IsDisposed)
  {
    for (int i = animationSteps; i > 0; i--)
    {
      Opacity = i / (float) animationSteps;
      Sleep(200 / animationSteps);
    }
  }
  Hide();
}

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

void PopoverControl::ComputeOutline()
{
  // Generate the outline of the actual content area.
  outline = gcnew GraphicsPath();

  System::Drawing::Rectangle bounds = System::Drawing::Rectangle(0, 0, Width, Height);
  System::Drawing::Point localHotSpot = PointToClient(hotSpot);

  switch (relativePosition)
  {
  case mforms::Left:
    {
      outline->AddArc(bounds.Left, bounds.Top, cornerSize, cornerSize, 180, 90);
      outline->AddArc(bounds.Right - cornerSize - ARROW_SIZE, bounds.Top, cornerSize, cornerSize, -90, 90);

      // Arrow.
      outline->AddLine(bounds.Right - ARROW_SIZE, bounds.Top + cornerSize, bounds.Right - ARROW_SIZE,
        localHotSpot.Y - ARROW_BASE / 2);
      outline->AddLine(bounds.Right - ARROW_SIZE, localHotSpot.Y - ARROW_BASE / 2, bounds.Right, localHotSpot.Y);
      outline->AddLine(bounds.Right, localHotSpot.Y, bounds.Right - ARROW_SIZE,
        localHotSpot.Y + ARROW_BASE / 2);
      outline->AddLine(bounds.Right - ARROW_SIZE, localHotSpot.Y + ARROW_BASE / 2,
        bounds.Right - ARROW_SIZE, bounds.Bottom - cornerSize);

      outline->AddArc(bounds.Right - cornerSize - ARROW_SIZE, bounds.Bottom - cornerSize,
        cornerSize, cornerSize, 0, 90);
      outline->AddArc(bounds.Left, bounds.Bottom - cornerSize, cornerSize, cornerSize, 90, 90);
      break;
    }
  case mforms::Right:
    {
      outline->AddArc(bounds.Left + ARROW_SIZE, bounds.Top, cornerSize, cornerSize, 180, 90);
      outline->AddArc(bounds.Right - cornerSize, bounds.Top, cornerSize, cornerSize, -90, 90);
      outline->AddArc(bounds.Right - cornerSize, bounds.Bottom - cornerSize,
        cornerSize, cornerSize, 0, 90);
      outline->AddArc(bounds.Left + ARROW_SIZE, bounds.Bottom - cornerSize, cornerSize, cornerSize, 90, 90);

      // Arrow.
      outline->AddLine(bounds.Left + ARROW_SIZE, bounds.Bottom - cornerSize, bounds.Left + ARROW_SIZE,
        localHotSpot.Y + ARROW_BASE / 2);
      outline->AddLine(bounds.Left + ARROW_SIZE, localHotSpot.Y + ARROW_BASE / 2, bounds.Left, localHotSpot.Y);
      outline->AddLine(bounds.Left, localHotSpot.Y, bounds.Left + ARROW_SIZE,
        localHotSpot.Y - ARROW_BASE / 2);
      outline->AddLine(bounds.Left + ARROW_SIZE, localHotSpot.Y - ARROW_BASE / 2,
        bounds.Left + ARROW_SIZE, bounds.Top + cornerSize);

      break;
    }
  case mforms::Above:
    {
      outline->AddArc(bounds.Left, bounds.Top, cornerSize, cornerSize, 180, 90);
      outline->AddArc(bounds.Right - cornerSize, bounds.Top, cornerSize, cornerSize, -90, 90);
      outline->AddArc(bounds.Right - cornerSize, bounds.Bottom - cornerSize - ARROW_SIZE,
        cornerSize, cornerSize, 0, 90);

      // Arrow.
      outline->AddLine(bounds.Right - cornerSize, bounds.Bottom - ARROW_SIZE, localHotSpot.X + ARROW_BASE / 2,
        bounds.Bottom - ARROW_SIZE);
      outline->AddLine(localHotSpot.X + ARROW_BASE / 2, bounds.Bottom - ARROW_SIZE,
        localHotSpot.X, bounds.Bottom);
      outline->AddLine(localHotSpot.X, bounds.Bottom, localHotSpot.X - ARROW_BASE / 2,
        bounds.Bottom - ARROW_SIZE);
      outline->AddLine(localHotSpot.X - ARROW_BASE / 2, bounds.Bottom - ARROW_SIZE,
        bounds.Left + cornerSize, bounds.Bottom - ARROW_SIZE);

      outline->AddArc(bounds.Left, bounds.Bottom - cornerSize - ARROW_SIZE, cornerSize, cornerSize, 90, 90);
      break;
    }
  case mforms::Below:
    {
      outline->AddArc(bounds.Left, bounds.Top + ARROW_SIZE, cornerSize, cornerSize, 180, 90);

      // Arrow.
      outline->AddLine(bounds.Left + cornerSize, bounds.Top + ARROW_SIZE, localHotSpot.X - ARROW_BASE / 2,
        bounds.Top + ARROW_SIZE);
      outline->AddLine(localHotSpot.X - ARROW_BASE / 2, bounds.Top + ARROW_SIZE,
        localHotSpot.X, bounds.Top);
      outline->AddLine(localHotSpot.X, bounds.Top, localHotSpot.X + ARROW_BASE / 2, bounds.Top + ARROW_SIZE);
      outline->AddLine(localHotSpot.X + ARROW_BASE / 2, bounds.Top + ARROW_SIZE,
        bounds.Right - cornerSize, bounds.Top + ARROW_SIZE);

      outline->AddArc(bounds.Right - cornerSize, bounds.Top + ARROW_SIZE, cornerSize, cornerSize, -90, 90);
      outline->AddArc(bounds.Right - cornerSize, bounds.Bottom - cornerSize,
        cornerSize, cornerSize, 0, 90);
      outline->AddArc(bounds.Left, bounds.Bottom - cornerSize, cornerSize, cornerSize, 90, 90);
      break;
    }
  }
  outline->CloseAllFigures();
}

//----------------- PopoverImpl --------------------------------------------------------------------

PopoverImpl::PopoverImpl(mforms::Popover *self)
  : ObjectImpl(self)
{
}


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

PopoverImpl::~PopoverImpl()
{
}

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

bool PopoverImpl::create(mforms::Popover *self)
{
  PopoverImpl^ popover = gcnew PopoverImpl(self);
  PopoverControl^ control = ObjectImpl::create<PopoverControl>(self, popover);

  return true;
}

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

void PopoverImpl::set_content(mforms::Popover *self, mforms::View* content)
{
  PopoverImpl^ popover = (PopoverImpl^)ObjectImpl::FromUnmanaged(self);

  ViewImpl^ child = (ViewImpl^)ObjectImpl::FromUnmanaged(content);
  Control^ ctl = child->get_control<Control>();
  ctl->Dock = DockStyle::Fill;

  Control^ host = popover->get_control<Control>();
  host->Controls->Add(ctl);

}

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

void PopoverImpl::set_size(mforms::Popover *self, int width, int height)
{
  PopoverImpl^ popover = (PopoverImpl^)ObjectImpl::FromUnmanaged(self);

  PopoverControl^ control= popover->get_control<PopoverControl>();
  control->SetBaseSize(width, height);
}

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

void PopoverImpl::show(mforms::Popover *self, int spot_x, int spot_y, mforms::StartPosition position)
{
  PopoverImpl^ popover = (PopoverImpl^)ObjectImpl::FromUnmanaged(self);

  PopoverControl^ control= popover->get_control<PopoverControl>();
  control->Show(spot_x, spot_y, position);
}

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

MySQL::Geometry::Rect PopoverImpl::get_content_rect(mforms::Popover *self)
{
  PopoverImpl^ popover = (PopoverImpl^)ObjectImpl::FromUnmanaged(self);

  PopoverControl^ control= popover->get_control<PopoverControl>();
  return control->DisplayRect;
}

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

void PopoverImpl::close(mforms::Popover *self)
{
  PopoverImpl^ popover = (PopoverImpl^)ObjectImpl::FromUnmanaged(self);

  PopoverControl^ control= popover->get_control<PopoverControl>();
  control->HidePopup();
}

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