// This file is part of VSTGUI. It is subject to the license terms 
// in the LICENSE file found in the top-level directory of this
// distribution and at http://github.com/steinbergmedia/vstgui/LICENSE

#include "window.h"
#include "application.h"
#include "../../lib/cframe.h"
#include "../../lib/controls/coptionmenu.h"
#include "../../lib/dispatchlist.h"
#include "../../lib/events.h"
#include "../../uidescription/icontroller.h"
#include "../include/iapplication.h"
#include "../include/icommand.h"
#include "../include/ipreference.h"
#include "../include/iwindowcontroller.h"
#include "platform/iplatformwindow.h"

#include <sstream>
#include <vector>

//------------------------------------------------------------------------
namespace VSTGUI {
namespace Standalone {
namespace Detail {
namespace /*anonymous*/ {

//------------------------------------------------------------------------
UTF8String strFromPositionAndSize (const CPoint& pos, const CPoint& size)
{
	std::stringstream str;
	str << pos.x << "," << pos.y << "," << size.x << "," << size.y;
	return UTF8String (str.str ());
}

//------------------------------------------------------------------------
struct PosAndSize
{
	CPoint pos;
	CPoint size;
};

static CPoint nullPoint {0, 0};

//------------------------------------------------------------------------
PosAndSize positionAndSizeFromString (const UTF8String& str)
{
	PosAndSize r;
	std::vector<std::string> elements;
	std::stringstream stream (str.getString ());
	std::string item;
	while (std::getline (stream, item, ','))
		elements.emplace_back (item);

	if (elements.size () != 4)
		return r;
	r.pos.x = UTF8StringView (elements[0].data ()).toDouble ();
	r.pos.y = UTF8StringView (elements[1].data ()).toDouble ();
	r.size.x = UTF8StringView (elements[2].data ()).toDouble ();
	r.size.y = UTF8StringView (elements[3].data ()).toDouble ();

	return r;
}

//------------------------------------------------------------------------
} // anonymous

//------------------------------------------------------------------------
class Window : public IPlatformWindowAccess,
               public Platform::IWindowDelegate,
               public IMouseObserver,
               public std::enable_shared_from_this<Window>
{
public:
	bool init (const WindowConfiguration& config, const WindowControllerPtr& controller);

	// IWindow
	const WindowControllerPtr& getController () const override { return controller; }
	CPoint getSize () const override { return platformWindow->getSize (); }
	CPoint getPosition () const override { return platformWindow->getPosition (); }
	double getScaleFactor () const override { return platformWindow->getScaleFactor (); }
	CRect getFocusViewRect () const override;
	const UTF8String& getTitle () const override { return title; }
	WindowType getType () const override { return windowType; }
	WindowStyle getStyle () const override { return windowStyle; }
	const UTF8String& getAutoSaveFrameName () const override { return autoSaveFrameName; }
	void setAutoSaveFrameName (const UTF8String& name) override { autoSaveFrameName = name; }
	void setSize (const CPoint& newSize) override;
	void setPosition (const CPoint& newPosition) override
	{
		platformWindow->setPosition (newPosition);
	}
	void setTitle (const UTF8String& newTitle) override
	{
		title = newTitle;
		platformWindow->setTitle (newTitle);
	}
	void setContentView (const SharedPointer<CFrame>& newFrame) override;
	void setRepresentedPath (const UTF8String& path) override;
	WindowStyle changeStyle (WindowStyle stylesToAdd, WindowStyle stylesToRemove) override;
	void show () override;
	void hide () override { platformWindow->hide (); }
	void close () override { platformWindow->close (); }
	void activate () override { platformWindow->activate (); }
	void registerWindowListener (IWindowListener* listener) override;
	void unregisterWindowListener (IWindowListener* listener) override;

	// IPlatformWindowAccess
	InterfacePtr getPlatformWindow () const override { return platformWindow; }
	CFrame* getFrame () const override { return frame; }

	// Platform::IWindowDelegate
	CPoint constraintSize (const CPoint& newSize) override;
	void onSizeChanged (const CPoint& newSize) override;
	void onPositionChanged (const CPoint& newPosition) override;
	void onShow () override;
	void onHide () override;
	void onClosed () override;
	bool canClose () override;
	void onActivated () override;
	void onDeactivated () override;
	// ICommandHandler
	bool canHandleCommand (const Command& command) override;
	bool handleCommand (const Command& command) override;

	// IMouseObserver
	void onMouseEntered (CView*, CFrame* ) override {};
	void onMouseExited (CView*, CFrame* ) override {};
	void onMouseEvent (MouseEvent& event, CFrame*) override;

private:
	WindowControllerPtr controller;
	WindowStyle windowStyle;
	WindowType windowType;
	Platform::WindowPtr platformWindow;
	SharedPointer<CFrame> frame;
	UTF8String autoSaveFrameName;
	UTF8String title;
	DispatchList<IWindowListener*> windowListeners;
};

//------------------------------------------------------------------------
bool Window::init (const WindowConfiguration& config, const WindowControllerPtr& inController)
{
	title = config.title;
	windowStyle = config.style;
	windowType = config.type;
	platformWindow = Platform::makeWindow (config, *this);
	if (platformWindow)
	{
		if (!config.autoSaveFrameName.empty ())
		{
			autoSaveFrameName = config.autoSaveFrameName;
		}
		controller = inController;
	}
	return platformWindow != nullptr;
}

//------------------------------------------------------------------------
void Window::setSize (const CPoint& newSize)
{
	CPoint size (newSize);
	if (controller)
		size = controller->constraintSize (*this, size);
	platformWindow->setSize (size);
}

//------------------------------------------------------------------------
void Window::show ()
{
	if (controller)
		controller->beforeShow (*this);
	bool positionChanged = false;
	if (!autoSaveFrameName.empty ())
	{
		if (auto frameName = IApplication::instance ().getPreferences ().get (autoSaveFrameName))
		{
			auto ps = positionAndSizeFromString (*frameName);
			if (ps.pos != nullPoint && ps.size != nullPoint)
			{
				setPosition (ps.pos);
				setSize (ps.size);
				positionChanged = true;
			}
		}
	}
	if (!positionChanged && windowStyle.isCentered ())
		platformWindow->center ();
	platformWindow->show ();
}

//------------------------------------------------------------------------
void Window::setContentView (const SharedPointer<CFrame>& newFrame)
{
	if (frame)
	{
		frame->unregisterMouseObserver (this);
		frame->close ();
	}
	frame = newFrame;
	if (!frame)
	{
		if (controller)
			controller->onSetContentView (*this, frame);
		return;
	}
	auto frameConfig =
	    controller ? controller->createPlatformFrameConfig (platformWindow->getPlatformType ()) :
	                 nullptr;
	frameConfig = platformWindow->prepareFrameConfig (std::move (frameConfig));
	frame->open (platformWindow->getPlatformHandle (), platformWindow->getPlatformType (),
	             frameConfig.get ());
	frame->registerMouseObserver (this);
	platformWindow->onSetContentView (frame);
	if (controller)
		controller->onSetContentView (*this, frame);
}

//------------------------------------------------------------------------
void Window::setRepresentedPath (const UTF8String& path)
{
	platformWindow->setRepresentedPath (path);
}

//------------------------------------------------------------------------
WindowStyle Window::changeStyle (WindowStyle stylesToAdd, WindowStyle stylesToRemove)
{
	windowStyle = platformWindow->changeStyle (stylesToAdd, stylesToRemove);
	return windowStyle;
}


//------------------------------------------------------------------------
CRect Window::getFocusViewRect () const
{
	CRect result;
	if (frame)
	{
		if (auto focusView = frame->getFocusView ())
		{
			result = focusView->getViewSize ();
			focusView->translateToGlobal (result);
		}
	}
	return result;
}

//------------------------------------------------------------------------
CPoint Window::constraintSize (const CPoint& _newSize)
{
	CPoint newSize (_newSize);
	if (frame)
		newSize = frame->checkSizeConstraint (newSize);
	return controller ? controller->constraintSize (*this, newSize) : newSize;
}

//------------------------------------------------------------------------
void Window::onSizeChanged (const CPoint& newSize)
{
	windowListeners.forEach (
	    [&] (IWindowListener* listener) { listener->onSizeChanged (*this, newSize); });
	if (controller)
		controller->onSizeChanged (*this, newSize);
}

//------------------------------------------------------------------------
void Window::onPositionChanged (const CPoint& newPosition)
{
	windowListeners.forEach (
	    [&] (IWindowListener* listener) { listener->onPositionChanged (*this, newPosition); });
	if (controller)
		controller->onPositionChanged (*this, newPosition);
}

//------------------------------------------------------------------------
void Window::onClosed ()
{
	auto self = shared_from_this (); // make sure we live as long as this method executes

	if (!autoSaveFrameName.empty ())
		IApplication::instance ().getPreferences ().set (
		    autoSaveFrameName, strFromPositionAndSize (getPosition (), getSize ()));

	windowListeners.forEach ([&] (IWindowListener* listener) {
		listener->onClosed (*this);
		windowListeners.remove (listener);
	});
	if (controller)
		controller->onClosed (*this);
	platformWindow->onSetContentView (nullptr);
	if (frame)
	{
		frame->unregisterMouseObserver (this);
		frame->remember ();
		frame->close ();
		frame = nullptr;
	}
	controller = nullptr;
	platformWindow = nullptr;
}

//------------------------------------------------------------------------
void Window::onShow ()
{
	windowListeners.forEach ([&] (IWindowListener* listener) { listener->onShow (*this); });
	if (controller)
		controller->onShow (*this);
}

//------------------------------------------------------------------------
void Window::onHide ()
{
	windowListeners.forEach ([&] (IWindowListener* listener) { listener->onHide (*this); });
	if (controller)
		controller->onHide (*this);
}

//------------------------------------------------------------------------
bool Window::canClose ()
{
	return controller ? controller->canClose (*this) : true;
}

//------------------------------------------------------------------------
void Window::onActivated ()
{
	windowListeners.forEach ([&] (IWindowListener* listener) { listener->onActivated (*this); });
	if (controller)
		controller->onActivated (*this);
}

//------------------------------------------------------------------------
void Window::onDeactivated ()
{
	windowListeners.forEach ([&] (IWindowListener* listener) { listener->onDeactivated (*this); });
	if (controller)
		controller->onDeactivated (*this);
}

//------------------------------------------------------------------------
void Window::registerWindowListener (IWindowListener* listener)
{
	windowListeners.add (listener);
}

//------------------------------------------------------------------------
void Window::unregisterWindowListener (IWindowListener* listener)
{
	windowListeners.remove (listener);
}

//------------------------------------------------------------------------
bool Window::canHandleCommand (const Command& command)
{
	if (command == Commands::CloseWindow)
		return controller->canClose (*this);
	if (auto focusView = frame->getFocusView ())
	{
		if (auto viewController = getViewController (focusView, false))
		{
			if (auto commandHandler = dynamic_cast<ICommandHandler*> (viewController))
			{
				if (commandHandler->canHandleCommand (command))
					return true;
			}
		}
	}
	if (auto commandHandler = dynamicPtrCast<ICommandHandler> (controller))
	{
		if (commandHandler->canHandleCommand (command))
			return true;
	}
	return false;
}

//------------------------------------------------------------------------
bool Window::handleCommand (const Command& command)
{
	if (command == Commands::CloseWindow)
	{
		close ();
		return true;
	}
	if (auto focusView = frame->getFocusView ())
	{
		if (auto viewController = getViewController (focusView, false))
		{
			if (auto commandHandler = dynamic_cast<ICommandHandler*> (viewController))
			{
				if (commandHandler->handleCommand (command))
					return true;
			}
		}
	}
	if (auto commandHandler = dynamicPtrCast<ICommandHandler> (controller))
	{
		if (commandHandler->handleCommand (command))
			return true;
	}
	return false;
}

//------------------------------------------------------------------------
struct WindowContextMenuCommandHandler : ICommandMenuItemTarget, NonAtomicReferenceCounted
{
	WindowContextMenuCommandHandler (Window* window) : window (window) {}
	bool validateCommandMenuItem (CCommandMenuItem* item) override
	{
		Command cmd = {item->getCommandCategory (), item->getCommandName ()};
		if (window->canHandleCommand (cmd) || getApplicationPlatformAccess ()->canHandleCommand (cmd))
			return true;
		item->setEnabled (false);
		return false;
	}
	bool onCommandMenuItemSelected (CCommandMenuItem* item) override
	{
		Command cmd = {item->getCommandCategory (), item->getCommandName ()};
		if (window->handleCommand (cmd))
			return true;
		return getApplicationPlatformAccess ()->handleCommand (cmd);
	}

	Window* window;
};


//------------------------------------------------------------------------
void Window::onMouseEvent (MouseEvent& event, CFrame* inFrame)
{
	if (event.type != EventType::MouseDown || !event.buttonState.isRight ())
		return;

	auto contextMenu = makeOwned<COptionMenu> ();

	CPoint where (event.mousePosition);
	inFrame->getTransform ().transform (where);

	CViewContainer::ViewList views;
	if (inFrame->getViewsAt (where, views, GetViewOptions ().deep ().includeViewContainer ()))
	{
		for (const auto& view : views)
		{
			auto viewController = getViewController (view);
			auto contextMenuController = dynamic_cast<IContextMenuController*> (viewController);
			auto contextMenuController2 = dynamic_cast<IContextMenuController2*> (viewController);
			if (contextMenuController == nullptr && contextMenuController2 == nullptr)
				continue;
			if (contextMenu->getNbEntries () != 0)
				contextMenu->addSeparator ();
			CPoint p (event.mousePosition);
			view->frameToLocal (p);
			if (contextMenuController2)
				contextMenuController2->appendContextMenuItems (*contextMenu, view, p);
			else if (contextMenuController)
				contextMenuController->appendContextMenuItems (*contextMenu, p);
		}
	}
	if (contextMenu->getNbEntries () == 0 &&
	    getApplicationPlatformAccess ()->getConfiguration ().showCommandsInWindowContextMenu)
	{
		auto commandList = getApplicationPlatformAccess ()->getCommandList (
		    staticPtrCast<Platform::IWindow> (getPlatformWindow ()).get ());
		if (!commandList.empty ())
		{
			auto menuHandler = makeOwned<WindowContextMenuCommandHandler> (this);
			for (const auto& cat : commandList)
			{
				auto item = new CMenuItem (cat.first);
				auto catMenu = new COptionMenu ();
				item->setSubmenu (catMenu);
				for (const auto& entry : cat.second)
				{
					if (entry.name == CommandName::MenuSeparator)
					{
						catMenu->addSeparator ();
					}
					else
					{
						auto catItem =
							new CCommandMenuItem ({entry.name, menuHandler, entry.group, entry.name});
						catMenu->addEntry (catItem);
					}
				}
				if (catMenu->getNbEntries () > 0)
					contextMenu->addEntry (item);
				else
					item->forget ();
			}
		}
	}

	if (contextMenu->getNbEntries () > 0)
	{
		contextMenu->cleanupSeparators (true);
		contextMenu->setStyle (COptionMenu::kPopupStyle | COptionMenu::kMultipleCheckStyle);
		contextMenu->popup (inFrame, event.mousePosition);
		event.consumed = true;
		castMouseDownEvent (event).ignoreFollowUpMoveAndUpEvents (true);
	}
}

//------------------------------------------------------------------------
WindowPtr makeWindow (const WindowConfiguration& config, const WindowControllerPtr& controller)
{
	auto window = std::make_shared<Detail::Window> ();
	if (!window->init (config, controller))
		return nullptr;
	return window;
}

//------------------------------------------------------------------------
} // Detail
} // Standalone
} // VSTGUI
