/*******************************************************************************
 * disp_sdl.cpp
 *
 * Written by Christoph Hormann <chris_hormann@gmx.de>
 *
 * SDL (Simple direct media layer) based render display system
 *
 * ---------------------------------------------------------------------------
 * Persistence of Vision Ray Tracer ('POV-Ray') version 3.7.
 * Copyright 1991-2013 Persistence of Vision Raytracer Pty. Ltd.
 *
 * POV-Ray is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * POV-Ray 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------------
 * POV-Ray is based on the popular DKB raytracer version 2.12.
 * DKBTrace was originally written by David K. Buck.
 * DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins.
 * ---------------------------------------------------------------------------
 * $File: //depot/public/povray/3.x/unix/disp_sdl.cpp $
 * $Revision: #1 $
 * $Change: 6069 $
 * $DateTime: 2013/11/06 11:59:40 $
 * $Author: chrisc $
 *******************************************************************************/

#include "config.h"

#ifdef HAVE_LIBSDL

#include "disp_sdl.h"

#include <algorithm>

// this must be the last file included
#include "syspovdebug.h"


namespace pov_frontend
{
	using namespace std;
	using namespace vfe;
	using namespace vfePlatform;

	extern boost::shared_ptr<Display> gDisplay;

	const UnixOptionsProcessor::Option_Info UnixSDLDisplay::Options[] =
	{
		// command line/povray.conf/environment options of this display mode can be added here
		// section name, option name, default, has_param, command line parameter, environment variable name, help text
		UnixOptionsProcessor::Option_Info("display", "scaled", "on", false, "", "POV_DISPLAY_SCALED", "scale render view to fit screen"),
		UnixOptionsProcessor::Option_Info("", "", "", false, "", "", "") // has to be last
	};

	bool UnixSDLDisplay::Register(vfeUnixSession *session)
	{
		session->GetUnixOptions()->Register(Options);
		// TODO: correct display detection
		return true;
	}

	UnixSDLDisplay::UnixSDLDisplay(unsigned int w, unsigned int h, GammaCurvePtr gamma, vfeSession *session, bool visible) :
		UnixDisplay(w, h, gamma, session, visible)
	{
		m_valid = false;
		m_display_scaled = false;
		m_display_scale = 1.;
		m_screen = NULL;
		m_display = NULL;
	}

	UnixSDLDisplay::~UnixSDLDisplay()
	{
		Close();
	}

	void UnixSDLDisplay::Initialise()
	{
		if (m_VisibleOnCreation)
			Show();
	}

	void UnixSDLDisplay::Hide()
	{
	}

	bool UnixSDLDisplay::TakeOver(UnixDisplay *display)
	{
		UnixSDLDisplay *p = dynamic_cast<UnixSDLDisplay *>(display);
		if (p == NULL)
			return false;
		if ((GetWidth() != p->GetWidth()) || (GetHeight() != p->GetHeight()))
			return false;

		m_valid = p->m_valid;
		m_display_scaled = p->m_display_scaled;
		m_display_scale = p->m_display_scale;
		m_screen = p->m_screen;
		m_display = p->m_display;

		if (m_display_scaled)
		{
			int width = GetWidth();
			int height = GetHeight();
			// allocate a new pixel counters, dropping influence of previous picture
			m_PxCount.clear(); // not useful, vector was created empty, just to be sure 
			m_PxCount.reserve(width*height); // we need that, and the loop!
			for(vector<unsigned char>::iterator iter = m_PxCount.begin(); iter != m_PxCount.end(); iter++)
				(*iter) = 0;
		}

		return true;
	}

	void UnixSDLDisplay::Close()
	{
		if (!m_valid)
			return;

// FIXME: should handle this correctly for the last frame
//		SDL_FreeSurface(m_display);
//		SDL_Quit();
		m_PxCount.clear();
		m_valid = false;
	}

	void UnixSDLDisplay::SetCaption(bool paused)
	{
		if (!m_valid)
			return;

		boost::format f;
		if (m_display_scaled)
			f = boost::format(PACKAGE_NAME " " VERSION_BASE " SDL display (scaled at %.0f%%)%s")
				% (m_display_scale*100)
				% (paused ? " [paused]" : "");
		else
			f = boost::format(PACKAGE_NAME " " VERSION_BASE " SDL display%s")
				% (paused ? " [paused]" : "");
		// FIXME: SDL_WM_SetCaption() causes locks on some distros, see http://bugs.povray.org/23
		// FIXME: SDL_WM_SetCaption(f.str().c_str(), PACKAGE_NAME);
	}

	void UnixSDLDisplay::Show()
	{
		if (gDisplay.get() != this)
			gDisplay = m_Session->GetDisplay();

		if (!m_valid)
		{
			// Initialize SDL
			if ( SDL_Init(SDL_INIT_VIDEO) != 0 )
			{
				fprintf(stderr, "Couldn't initialize SDL: %s.\n", SDL_GetError());
				return;
			}

			int desired_bpp = 0;
			Uint32 video_flags = 0;
			int width = GetWidth();
			int height = GetHeight();

			vfeUnixSession *UxSession = dynamic_cast<vfeUnixSession *>(m_Session);

			if (UxSession->GetUnixOptions()->isOptionSet("display", "scaled"))
			// determine maximum display area (wrong and ugly)
			{
				SDL_Rect **modes = SDL_ListModes(NULL, SDL_FULLSCREEN);
        // [JG] about testing vs ...(-1), have a look at SDL_ListModes API (the return is very ugly).
				if ((modes != NULL)&&(reinterpret_cast<SDL_Rect**>(-1) != modes))
				{
					width = min(modes[0]->w - 10, width);
					height = min(modes[0]->h - 80, height);
				}
			}

			// calculate display area
			float AspectRatio = float(width)/float(height);
			float AspectRatio_Full = float(GetWidth())/float(GetHeight());
			if (AspectRatio > AspectRatio_Full)
				width = int(AspectRatio_Full*float(height));
			else if (AspectRatio != AspectRatio_Full)
				height = int(float(width)/AspectRatio_Full);

			// Initialize the display
			m_screen = SDL_SetVideoMode(width, height, desired_bpp, video_flags);
			if ( m_screen == NULL )
			{
				fprintf(stderr, "Couldn't set %dx%dx%d video mode: %s\n", width, height, desired_bpp, SDL_GetError());
				return;
			}

			SDL_Surface *temp = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);

			if ( temp == NULL )
			{
				fprintf(stderr, "Couldn't create render display surface: %s\n", SDL_GetError());
				return;
			}

			m_display = SDL_DisplayFormat(temp);
			SDL_FreeSurface(temp);

			if ( m_display == NULL )
			{
				fprintf(stderr, "Couldn't convert bar surface: %s\n", SDL_GetError());
				return;
			}

			m_PxCount.clear();
			m_PxCount.reserve(width*height);
			for(vector<unsigned char>::iterator iter = m_PxCount.begin(); iter != m_PxCount.end(); iter++)
				(*iter) = 0;

			m_update_rect.x = 0;
			m_update_rect.y = 0;
			m_update_rect.w = width;
			m_update_rect.h = height;

			m_screen_rect.x = 0;
			m_screen_rect.y = 0;
			m_screen_rect.w = width;
			m_screen_rect.h = height;

			m_valid = true;
			m_PxCnt = UpdateInterval;

			if ((width == GetWidth()) && (height == GetHeight()))					
			{
				m_display_scaled = false;
				m_display_scale = 1.;
			}
			else
			{
				m_display_scaled = true;
				m_display_scale = float(width) / GetWidth();
			}

			SetCaption(false);
		}
	}

	inline void UnixSDLDisplay::SetPixel(unsigned int x, unsigned int y, const RGBA8& colour)
	{
		Uint8 *p = (Uint8 *) m_display->pixels + y * m_display->pitch + x * m_display->format->BytesPerPixel;

		Uint32 sdl_col = SDL_MapRGBA(m_display->format, colour.red, colour.green, colour.blue, colour.alpha);

		switch (m_display->format->BytesPerPixel)
		{
			case 1:
				*p = sdl_col;
				break;
			case 2:
				*(Uint16 *) p = sdl_col;
				break;
			case 3:
				if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
				{
					p[0] = (sdl_col >> 16) & 0xFF;
					p[1] = (sdl_col >>  8) & 0xFF;
					p[2] =  sdl_col        & 0xFF;
				}
				else
				{
					p[0] =  sdl_col        & 0xFF;
					p[1] = (sdl_col >>  8) & 0xFF;
					p[2] = (sdl_col >> 16) & 0xFF;
				}
				break;
			case 4:
				*(Uint32 *) p = sdl_col;
				break;
		}
	}

	inline void UnixSDLDisplay::SetPixelScaled(unsigned int x, unsigned int y, const RGBA8& colour)
	{
		unsigned int ix = x * m_display_scale;
		unsigned int iy = y * m_display_scale;

		Uint8 *p = (Uint8 *) m_display->pixels + iy * m_display->pitch + ix * m_display->format->BytesPerPixel;

		Uint8 r, g, b, a;
		Uint32 old = *(Uint32 *) p;

		SDL_GetRGBA(old, m_display->format, &r, &g, &b, &a);

		unsigned int ofs = ix + iy * m_display->w;
		r = (r*m_PxCount[ofs] + colour.red  ) / (m_PxCount[ofs]+1);
		g = (g*m_PxCount[ofs] + colour.green) / (m_PxCount[ofs]+1);
		b = (b*m_PxCount[ofs] + colour.blue ) / (m_PxCount[ofs]+1);
		a = (a*m_PxCount[ofs] + colour.alpha) / (m_PxCount[ofs]+1);

		Uint32 sdl_col = SDL_MapRGBA(m_display->format, r, g, b, a);

		switch (m_display->format->BytesPerPixel)
		{
			case 1:
				*p = sdl_col;
				break;
			case 2:
				*(Uint16 *) p = sdl_col;
				break;
			case 3:
				if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
				{
					p[0] = (sdl_col >> 16) & 0xFF;
					p[1] = (sdl_col >>  8) & 0xFF;
					p[2] =  sdl_col        & 0xFF;
				}
				else
				{
					p[0] =  sdl_col        & 0xFF;
					p[1] = (sdl_col >>  8) & 0xFF;
					p[2] = (sdl_col >> 16) & 0xFF;
				}
				break;
			case 4:
				*(Uint32 *) p = sdl_col;
				break;
		}

		++m_PxCount[ofs];
	}

	void UnixSDLDisplay::UpdateCoord(unsigned int x, unsigned int y)
	{
		unsigned int rx2 = m_update_rect.x + m_update_rect.w;
		unsigned int ry2 = m_update_rect.y + m_update_rect.h;
		m_update_rect.x = min((unsigned int)m_update_rect.x, x);
		m_update_rect.y = min((unsigned int)m_update_rect.y, y);
		rx2 = max(rx2, x);
		ry2 = max(ry2, y);
		m_update_rect.w = rx2 - m_update_rect.x;
		m_update_rect.h = ry2 - m_update_rect.y;
	}

	void UnixSDLDisplay::UpdateCoord(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2)
	{
		unsigned int rx2 = m_update_rect.x + m_update_rect.w;
		unsigned int ry2 = m_update_rect.y + m_update_rect.h;
		m_update_rect.x = min((unsigned int)m_update_rect.x, x1);
		m_update_rect.y = min((unsigned int)m_update_rect.y, y1);
		rx2 = max(rx2, x2);
		ry2 = max(ry2, y2);
		m_update_rect.w = rx2 - m_update_rect.x;
		m_update_rect.h = ry2 - m_update_rect.y;
	}

	void UnixSDLDisplay::UpdateCoordScaled(unsigned int x, unsigned int y)
	{
		UpdateCoord(static_cast<unsigned int>(x * m_display_scale), static_cast<unsigned int>(y * m_display_scale));
	}

	void UnixSDLDisplay::UpdateCoordScaled(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2)
	{
		UpdateCoord(static_cast<unsigned int>(x1 * m_display_scale), static_cast<unsigned int>(y1 * m_display_scale),
		            static_cast<unsigned int>(x2 * m_display_scale), static_cast<unsigned int>(y2 * m_display_scale));
	}

	void UnixSDLDisplay::DrawPixel(unsigned int x, unsigned int y, const RGBA8& colour)
	{
		if (!m_valid || x >= GetWidth() || y >= GetHeight())
			return;
		if (SDL_MUSTLOCK(m_display) && SDL_LockSurface(m_display) < 0)
			return;

		if (m_display_scaled)
		{
			SetPixelScaled(x, y, colour);
			UpdateCoordScaled(x, y);
		}
		else
		{
			SetPixel(x, y, colour);
			UpdateCoord(x, y);
		}

		m_PxCnt++;

		if (SDL_MUSTLOCK(m_display))
			SDL_UnlockSurface(m_display);
	}

	void UnixSDLDisplay::DrawRectangleFrame(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2, const RGBA8& colour)
	{
		if (!m_valid)
			return;

		int ix1 = min(x1, GetWidth()-1);
		int ix2 = min(x2, GetWidth()-1);
		int iy1 = min(y1, GetHeight()-1);
		int iy2 = min(y2, GetHeight()-1);

		if (SDL_MUSTLOCK(m_display) && SDL_LockSurface(m_display) < 0)
			return;

		if (m_display_scaled)
		{
			for(unsigned int x = ix1; x <= ix2; x++)
			{
				SetPixelScaled(x, iy1, colour);
				SetPixelScaled(x, iy2, colour);
			}

			for(unsigned int y = iy1; y <= iy2; y++)
			{
				SetPixelScaled(ix1, y, colour);
				SetPixelScaled(ix2, y, colour);
			}
			UpdateCoordScaled(ix1, iy1, ix2, iy2);
		}
		else
		{
			for(unsigned int x = ix1; x <= ix2; x++)
			{
				SetPixel(x, iy1, colour);
				SetPixel(x, iy2, colour);
			}

			for(unsigned int y = iy1; y <= iy2; y++)
			{
				SetPixel(ix1, y, colour);
				SetPixel(ix2, y, colour);
			}
			UpdateCoord(ix1, iy1, ix2, iy2);
		}

		if (SDL_MUSTLOCK(m_display))
			SDL_UnlockSurface(m_display);

		m_PxCnt = UpdateInterval;
	}

	void UnixSDLDisplay::DrawFilledRectangle(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2, const RGBA8& colour)
	{
		if (!m_valid)
			return;

		unsigned int ix1 = min(x1, GetWidth()-1);
		unsigned int ix2 = min(x2, GetWidth()-1);
		unsigned int iy1 = min(y1, GetHeight()-1);
		unsigned int iy2 = min(y2, GetHeight()-1);

		if (m_display_scaled)
		{
			ix1 *= m_display_scale;
			iy1 *= m_display_scale;
			ix2 *= m_display_scale;
			iy2 *= m_display_scale;
		}

		UpdateCoord(ix1, iy1, ix2, iy2);

		Uint32 sdl_col = SDL_MapRGBA(m_display->format, colour.red, colour.green, colour.blue, colour.alpha);

        SDL_Rect tempRect;
        tempRect.x = ix1;
        tempRect.y = iy1;
        tempRect.w = ix2 - ix1 + 1;
        tempRect.h = iy2 - iy1 + 1;
        SDL_FillRect(m_display, &tempRect, sdl_col);

		m_PxCnt = UpdateInterval;
	}

	void UnixSDLDisplay::DrawPixelBlock(unsigned int x1, unsigned int y1, unsigned int x2, unsigned int y2, const RGBA8 *colour)
	{
		if (!m_valid)
			return;

		unsigned int ix1 = min(x1, GetWidth()-1);
		unsigned int ix2 = min(x2, GetWidth()-1);
		unsigned int iy1 = min(y1, GetHeight()-1);
		unsigned int iy2 = min(y2, GetHeight()-1);

		if (SDL_MUSTLOCK(m_display) && SDL_LockSurface(m_display) < 0)
			return;

		if (m_display_scaled)
		{
			for(unsigned int y = iy1, i = 0; y <= iy2; y++)
				for(unsigned int x = ix1; x <= ix2; x++, i++)
					SetPixelScaled(x, y, colour[i]);
			UpdateCoordScaled(ix1, iy1, ix2, iy2);
		}
		else
		{
			for(unsigned int y = y1, i = 0; y <= iy2; y++)
				for(unsigned int x = ix1; x <= ix2; x++, i++)
					SetPixel(x, y, colour[i]);
			UpdateCoord(ix1, iy1, ix2, iy2);
		}

		if (SDL_MUSTLOCK(m_display))
			SDL_UnlockSurface(m_display);

		m_PxCnt = UpdateInterval;
	}

	void UnixSDLDisplay::Clear()
	{
		for(vector<unsigned char>::iterator iter = m_PxCount.begin(); iter != m_PxCount.end(); iter++)
			(*iter) = 0;

		m_update_rect.x = 0;
		m_update_rect.y = 0;
		m_update_rect.w = m_display->w;
		m_update_rect.h = m_display->h;

		SDL_FillRect(m_display, &m_update_rect, (Uint32)0);

		m_PxCnt = UpdateInterval;
	}

	void UnixSDLDisplay::UpdateScreen(bool Force = false)
	{
		if (!m_valid)
			return;
		if (Force || m_PxCnt >= UpdateInterval)
		{
			SDL_BlitSurface(m_display, &m_update_rect, m_screen, &m_update_rect);
			SDL_UpdateRect(m_screen, m_update_rect.x, m_update_rect.y, m_update_rect.w, m_update_rect.h);
			m_PxCnt = 0;
		}
	}

	void UnixSDLDisplay::PauseWhenDoneNotifyStart()
	{
		if (!m_valid)
			return;
		fprintf(stderr, "Press a key or click the display to continue...");
		SetCaption(true);
	}

	void UnixSDLDisplay::PauseWhenDoneNotifyEnd()
	{
		if (!m_valid)
			return;
		SetCaption(false);
		fprintf(stderr, "\n\n");
	}

	bool UnixSDLDisplay::PauseWhenDoneResumeIsRequested()
	{
		if (!m_valid)
			return true;

		SDL_Event event;
		bool do_quit = false;

		if (SDL_PollEvent(&event))
		{
			switch (event.type)
			{
				case SDL_KEYDOWN:
					if ( event.key.keysym.sym == SDLK_q || event.key.keysym.sym == SDLK_RETURN || event.key.keysym.sym == SDLK_KP_ENTER )
						do_quit = true;
					break;
				case SDL_MOUSEBUTTONDOWN:
					do_quit = true;
					break;
			}
		}

		return do_quit;
	}

	bool UnixSDLDisplay::HandleEvents()
	{
		if (!m_valid)
			return false;

		SDL_Event event; 
		bool do_quit = false;

		while (SDL_PollEvent(&event))
		{
			switch (event.type)
			{
				case SDL_KEYDOWN:
					if ( event.key.keysym.sym == SDLK_q )
						do_quit = true;
					else if ( event.key.keysym.sym == SDLK_p )
					{
						if (!m_Session->IsPausable())
							break;

						if (m_Session->Paused())
						{
							if (m_Session->Resume())
								SetCaption(false);
						}
						else
						{
							if (m_Session->Pause())
								SetCaption(true);
						}
					}
					break;
				case SDL_QUIT:
					do_quit = true;
					break;
			}
			if (do_quit)
				break;
		}

		return do_quit;
	}

}

#endif /* HAVE_LIBSDL */
