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

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

ref class MySQL::Forms::PopoverControl : Windows::Forms::Form
{
private:
  int cornerSize;
  int animationSteps;
  bool animated;
  Drawing::Point hotSpot;
  Drawing::Size baseSize;
  Bitmap ^contentBitmap;
  GraphicsPath ^outline;

  mforms::StartPosition relativePosition;
  mforms::PopoverStyle style;

protected:
  virtual void OnKeyPress(KeyPressEventArgs ^args) override
  {
    if (args->KeyChar == 27) // Escape char
      Close();

    __super::OnKeyPress(args);
  }

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

  virtual void OnPaint(PaintEventArgs ^args) override
  {
    // 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;

    args->Graphics->SmoothingMode = SmoothingMode::HighQuality;
    args->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);
    args->Graphics->DrawPath(borderPen, helper);

    delete borderPen;
  }

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

  void UpdateAndShowPopover(bool doAnimated)
  {
    ComputeOutline();
    Region = gcnew System::Drawing::Region(outline);

    // 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();
  }

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

  #define DEFAULT_PADDING 7 // Padding on all sides.

  #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.

  void ComputeCoordinatesAndPadding()
  {
    // The base size is the size of the main part, without arrow.
    System::Drawing::Size actualSize = baseSize;
    actualSize.Width += 2 * DEFAULT_PADDING;
    actualSize.Height += 2 * DEFAULT_PADDING;

    if (style == mforms::PopoverStyleNormal)
    {
      // 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 = hotSpot;
    if (style == mforms::PopoverStyleNormal)
    {
      switch (relativePosition)
      {
      case mforms::Left:
        newLocation.X = hotSpot.X - actualSize.Width;
        newLocation.Y = hotSpot.Y - actualSize.Height / 3;
        Padding = System::Windows::Forms::Padding(DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING + ARROW_SIZE, 7);
        break;
      case mforms::Right:
        newLocation.X = hotSpot.X;
        newLocation.Y = hotSpot.Y - actualSize.Height / 3;
        Padding = System::Windows::Forms::Padding(DEFAULT_PADDING + ARROW_SIZE, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING);
        break;
      case mforms::Above:
        newLocation.X = hotSpot.X - actualSize.Width / 3;
        newLocation.Y = hotSpot.Y - actualSize.Height;
        Padding = System::Windows::Forms::Padding(DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING, DEFAULT_PADDING + ARROW_SIZE);
        break;
      case mforms::Below:
        newLocation.X = hotSpot.X - actualSize.Width / 3;
        newLocation.Y = hotSpot.Y;
        Padding = System::Windows::Forms::Padding(DEFAULT_PADDING, DEFAULT_PADDING + ARROW_SIZE, DEFAULT_PADDING, DEFAULT_PADDING);
        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 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 (style)
    {
    case mforms::PopoverStyleTooltip:
      {
        outline->AddRectangle(bounds);

        break;
      }

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

        break;
      }
    }
    outline->CloseAllFigures();
  }

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

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

    cornerSize = 14;
    animationSteps = 8;
  }

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

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

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

  void DoRepaint()
  {
    Invalidate();
  }

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

  /**
   * Shows the popover with its hotspot at the given position. The window is moved accordingly and also
   * considers screen borders.
   */
  void Show(int x, int y, mforms::StartPosition position)
  {
    if (x < 0 && y < 0)
    {
      x = ::Cursor::Position.X + 8;
      y = ::Cursor::Position.Y + 8;
    }

    // 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 HidePopup()
  {
    if (animated && !IsDisposed)
    {
      for (int i = animationSteps; i > 0; i--)
      {
        Opacity = i / (float) animationSteps;
        Sleep(200 / animationSteps);
      }
    }
    Hide();
  }

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

  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 bool ShowWithoutActivation
  {
    bool get() override { return true; }
  }

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

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

      cp->ExStyle |= (int) MySQL::Utilities::SysUtils::WS::EX_NOACTIVATE;
      cp->ClassStyle |= CS_DROPSHADOW;

      return cp;
    }
  }

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

  property mforms::PopoverStyle Style
  {
    mforms::PopoverStyle get() { return style; };

    void set(mforms::PopoverStyle aStyle)
    {
      style = aStyle;
      switch (style)
      {
      case mforms::PopoverStyleTooltip:
        cornerSize = 0; // Not used currently.
        break;

      default:
        cornerSize = 14;
        break;
      }
    };
  }

};

//----------------- PopoverWrapper -----------------------------------------------------------------

PopoverWrapper::PopoverWrapper(mforms::Popover *backend)
  : ObjectWrapper(backend)
{
}


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

bool PopoverWrapper::create(mforms::Popover *backend, mforms::PopoverStyle style)
{
  PopoverWrapper *wrapper = new PopoverWrapper(backend);
  PopoverControl ^control = PopoverWrapper::Create<PopoverControl>(backend, wrapper);
  control->Style = style;

  return true;
}

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

void PopoverWrapper::destroy(mforms::Popover *backend)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  PopoverWrapper *wrapper = PopoverWrapper::GetWrapper<PopoverWrapper>(popover);
  wrapper->_track_connection.disconnect();
}

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

void PopoverWrapper::set_content(mforms::Popover *backend, mforms::View *content)
{
  Control ^child = PopoverWrapper::GetControl(content);
  child->Dock = DockStyle::Fill;

  Control^ host = PopoverWrapper::GetControl(backend);
  host->Controls->Add(child);

}

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

void PopoverWrapper::set_size(mforms::Popover *backend, int width, int height)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  popover->SetBaseSize(width, height);
}

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

void PopoverWrapper::show(mforms::Popover *backend, int spot_x, int spot_y, mforms::StartPosition position)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  popover->Show(spot_x, spot_y, position);
}

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

void PopoverWrapper::show_and_track(mforms::Popover *backend, mforms::View *owner, int spot_x, int spot_y,
  mforms::StartPosition position)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  PopoverWrapper *wrapper = PopoverWrapper::GetWrapper<PopoverWrapper>(popover);
  wrapper->_track_connection = owner->signal_mouse_leave()->connect(boost::bind(&PopoverWrapper::mouse_left_tracked_object, wrapper));

  popover->Show(spot_x, spot_y, position);
}

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

bool PopoverWrapper::mouse_left_tracked_object()
{
  _track_connection.disconnect();

  mforms::Popover *popover = GetBackend<mforms::Popover>();
  (*popover->signal_close())();

  return false;
}

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

base::Rect PopoverWrapper::get_content_rect(mforms::Popover *backend)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  return popover->DisplayRect;
}

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

void PopoverWrapper::close(mforms::Popover *backend)
{
  PopoverControl^ popover = PopoverWrapper::GetManagedObject<PopoverControl>(backend);
  popover->HidePopup();
}

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

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

  f->_popover_impl.create = &PopoverWrapper::create;
  f->_popover_impl.destroy =  &PopoverWrapper::destroy;
  f->_popover_impl.set_content = &PopoverWrapper::set_content;
  f->_popover_impl.set_size = &PopoverWrapper::set_size;
  f->_popover_impl.show = &PopoverWrapper::show;
  f->_popover_impl.show_and_track = &PopoverWrapper::show_and_track;
  f->_popover_impl.close = &PopoverWrapper::close;
}

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