//  BMP
//  Copyright (C) 2005-2007 BMP development.
//
//  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; either version 2 of the License, or
//  (at your option) any later version.
//
//  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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif // HAVE_CONFIG_H

#include <iostream>

#include <gtkmm.h>
#include <glibmm/i18n.h>
#include <gdkmm.h>
#include <pangomm.h>
#include <cairomm/cairomm.h>
#include <boost/format.hpp>
#include <sigc++/sigc++.h>

#include <cmath>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <gdk/gdkx.h>

#include "bmp/types/types-ui.hh"

#include "src/paths.hh"
#include "src/ui-tools.hh"

#include "popup.hh"

using namespace Glib;
using namespace Gtk;

namespace Bmp
{
	namespace
	{
		const int popup_width         = 480;
		const int popup_height        = 120;

		const int popup_animation_fps = 25;
		const int popup_animation_frame_period_ms = 1000 / popup_animation_fps;

		const double popup_full_alpha    = 1.0;
		const double popup_fade_in_time  = 0.15;
		const double popup_fade_out_time = 0.30;
		const double popup_hold_time     = 5.0;

		const double popup_total_time_no_fade = popup_hold_time;
		const double popup_total_time_fade    = popup_fade_in_time + popup_hold_time + popup_fade_out_time;

		const int arrow_width  = 26;
		const int arrow_height = 18;

		inline double
			cos_smooth (double x)
		{
			return (1.0 - std::cos (x * G_PI)) / 2.0;
		}

		double
			get_popup_alpha_at_time (double time,
			bool   fading = true)
		{
			if (fading)
			{
				if (time >= 0.0 && popup_total_time_fade)
				{
					if (time < popup_fade_in_time)
					{
						return popup_full_alpha * cos_smooth (time / popup_fade_in_time);
					}
					else if (time < popup_fade_in_time + popup_hold_time)
					{
						return popup_full_alpha;
					}
					else
					{
						time -= popup_fade_in_time + popup_hold_time;
						return popup_full_alpha * cos_smooth (1.0 - time / popup_fade_out_time);
					}
				}
			}
			else
			{
				if (time >= 0.0 && time < popup_total_time_no_fade)
					return popup_full_alpha;
			}

			return 0.0;
		}

		inline double
			get_popup_end_time (bool fading = true)
		{
			return fading ? popup_total_time_fade : popup_total_time_no_fade;
		}

		inline double
			get_popup_time_offset (bool fading = true)
		{
			return fading ? popup_fade_in_time : 0.0;
		}

		inline double
			get_popup_disappear_start_time (bool fading = true)
		{
			return fading ? popup_fade_in_time + popup_hold_time : get_popup_end_time (fading);
		}

		void
			window_set_opacity (Glib::RefPtr<Gdk::Window> const& window,
			double                           d)
		{
			const unsigned int opaque= 0xffffffff;
			const char * opacity_prop = "_NET_WM_WINDOW_OPACITY";

			unsigned int opacity = (unsigned int) (d * opaque);

			::Display * dpy = gdk_x11_display_get_xdisplay (gdk_display_get_default());
			::Window    win = GDK_WINDOW_XID (window->gobj());

			if (opacity == opaque)
			{
				XDeleteProperty (dpy, win, XInternAtom (dpy, opacity_prop, False));
			}
			else
			{
				XChangeProperty (dpy, win, XInternAtom (dpy, opacity_prop, False),
					XA_CARDINAL, 32, PropModeReplace,
					reinterpret_cast<unsigned char *> (&opacity), 1L);
				XSync (dpy, False);
			}
		}

	}							 // anonymous

	Popup::Popup (GtkWidget* widget, TrackMetadata const& metadata)
    : Window          (WINDOW_POPUP)
    , m_widget        (widget)
    , m_outline       (Gdk::Color ("#303030"))
    , m_inlay         (Gdk::Color ("#FFFFFF"))
    , m_image         (RefPixbuf(0))
    , m_source_icon   (RefPixbuf(0))
    , m_time_offset   (0.)
    , m_has_alpha     (Util::has_alpha())
    , m_width         (popup_width)
    , m_height        (popup_height)
    , m_ax            (40)
    , m_x             (0)
    , m_y             (0)
    , m_location      (ARROW_BOTTOM)
    , m_position      (ARROW_POS_DEFAULT)
    , m_tooltip_mode  (false)
    , m_metadata      (metadata)
	{
		if (m_has_alpha) set_colormap (Util::get_rgba_colormap());

		std::string family = get_pango_context()->get_font_description().get_family();

		m_layout = Pango::Layout::create (get_pango_context ());
		m_layout->set_ellipsize (Pango::ELLIPSIZE_END);
		m_layout->set_width ((m_width - 144) * PANGO_SCALE);

		PangoFontDescription * desc = pango_font_description_new ();

		pango_font_description_set_absolute_size (desc, 12 * PANGO_SCALE);
		pango_font_description_set_family (desc, family.c_str());
		pango_layout_set_font_description (m_layout->gobj(), desc);

		pango_font_description_set_absolute_size (desc, 10 * PANGO_SCALE);
		gtk_widget_modify_font (GTK_WIDGET (m_progress.gobj()), desc);

		pango_font_description_free (desc);

		add_events (Gdk::ALL_EVENTS_MASK);

		set_app_paintable (true);
		set_resizable (false);
		set_decorated (false);
		set_size_request (m_width, m_height);

		add (m_fixed);
		if (m_has_alpha)
        {
            m_progress.set_colormap (Util::get_rgba_colormap());
            m_progress.set_app_paintable (1);
            m_progress.set_double_buffered (0);
        }

		m_fixed.set_size_request (m_width, m_height);
		m_progress.set_size_request (m_width - 102, 18);
		m_fixed.put (m_progress, 94, 78);
		m_fixed.set_has_window (false);
		m_fixed.show_all ();

		m_fade = Util::has_alpha();
		m_timer.stop ();
		m_timer.reset ();
	}

	void
		Popup::reposition ()
	{
		int x, y, width, height;
		acquire_widget_info (x, y, width, height);

		int new_x = x + width/2;
		int new_y = y - m_height;

		if (m_x != new_x || m_y != new_y)
		{
			m_x = new_x;
			m_y = new_y;

			m_location = ARROW_BOTTOM;
			m_position = ARROW_POS_DEFAULT;
			m_ax = 40;

			if (m_y < 0)
			{
				m_location = ARROW_TOP;
				m_y = y + height;
			}

			int screen_width = Gdk::Screen::get_default ()->get_width ();

			if (m_x + m_width > screen_width)
			{
				m_ax = m_width - 40;
				m_x = (screen_width - m_width) - (screen_width - m_x - 40);

				if (m_x + m_width > screen_width)
				{
					m_position = ARROW_POS_RIGHT;
					m_x -= (m_width - m_ax);
					m_ax  = 0;
				}
			}
			else
			{
				m_x -= 40;
				if (m_x < 0)
				{
					m_position = ARROW_POS_LEFT;
					m_ax  = 0;
					m_x  = 40 + m_x;
				}
			}

			move (m_x, m_y);
			update_mask ();
		}
	}

	void
		Popup::update_mask ()
	{
		if (!m_has_alpha && is_realized ())
		{
			Glib::RefPtr<Gdk::Bitmap> mask = Glib::RefPtr<Gdk::Bitmap>::cast_static (Gdk::Pixmap::create (RefPtr<Gdk::Drawable> (0), m_width, m_height, 1));

			Cairo::RefPtr<Cairo::Context> cr = mask->create_cairo_context ();

			int width, height;
			width = get_allocation ().get_width () - 2;
			height = get_allocation ().get_height ();

			draw_arrow_mask (cr, width, height);

			get_window ()->shape_combine_mask (mask, 0, 0);
		}
	}

	Popup::~Popup ()
	{
		// empty
	}

	bool
		Popup::on_button_press_event (GdkEventButton * event)
	{
		if (event->button == 1)
			disappear ();

		return false;
	}

	void
		Popup::draw_arrow (Cairo::RefPtr<Cairo::Context> & cr,
		int                             w,
		int                             h)
	{
		cr->save ();

		if (m_location == ARROW_TOP)
		{
			Cairo::Matrix matrix = { 1, 0, 0, -1, 0, h };
			cr->set_matrix (matrix);
		}

		cr->move_to (1, arrow_height);

		if (m_position == ARROW_POS_LEFT)
		{
			cr->rel_line_to (+w, 0);
			cr->rel_line_to (0, +(h-(2*arrow_height)));
			cr->rel_line_to (-(w-(arrow_width/2)), 0);
			cr->rel_line_to (-(arrow_width/2), +arrow_height);
			cr->rel_line_to (0, -(h-arrow_height));
		}
		else if (m_position == ARROW_POS_RIGHT)
		{
			cr->rel_line_to (+w, 0);
			cr->rel_line_to (0, +(h-arrow_height));
			cr->rel_line_to (-(arrow_width/2), -arrow_height);
			cr->rel_line_to (-(w-(arrow_width/2)), 0);
			cr->rel_line_to (0, -(h-(2*arrow_height)));
		}
		else
		{
			cr->rel_line_to (+w, 0);
			cr->rel_line_to (0, +(h-(2*arrow_height)));
			cr->rel_line_to (-((w-m_ax)-(arrow_width/2)), 0);
			cr->rel_line_to (-(arrow_width/2), +arrow_height);
			cr->rel_line_to (-(arrow_width/2), -arrow_height);
			cr->rel_line_to (-(m_ax-(arrow_width/2)), 0);
			cr->rel_line_to (0, -(h-(2*arrow_height)));
		}

		cr->restore ();
	}

	void
		Popup::draw_arrow_mask (Cairo::RefPtr<Cairo::Context> & cr,
		int                             w,
		int                             h)
	{
		cr->save ();

		// Clear the BG
		cr->set_operator (Cairo::OPERATOR_CLEAR);
		cr->set_source_rgba (1.0, 1.0, 1.0, 0.0);
		cr->paint ();

		cr->set_operator (Cairo::OPERATOR_SOURCE);
		cr->set_source_rgba (.0, .0, .0, 1.0);

		draw_arrow (cr, w, h);

		cr->fill ();

		cr->restore ();
	}

	void
		Popup::draw_arrow_outline (Cairo::RefPtr<Cairo::Context> & cr,
		int                             w,
		int                             h)
	{
		cr->save ();

		cr->set_line_width (1.5);
		cr->set_line_cap (Cairo::LINE_CAP_ROUND);
		cr->set_line_join (Cairo::LINE_JOIN_ROUND);
		cr->set_operator (Cairo::OPERATOR_SOURCE);

		Gdk::Cairo::set_source_color (cr, m_inlay);
		draw_arrow (cr, w, h);
		cr->fill_preserve ();

		Gdk::Cairo::set_source_color (cr, m_outline);
		cr->stroke();
		cr->restore ();
	}

	void
		Popup::set_position (guint64 position, guint64 duration)
	{
		static boost::format
			time_duration_f ("%d:%02d  ..  %d:%02d");

		static boost::format
			time_f ("%d:%02d");

		if (duration)
			m_progress.set_text ((time_duration_f % (position / 60) % (position % 60) % (duration / 60) % (duration % 60)).str());
		else
			m_progress.set_text ((time_f % (position / 60) % (position % 60)).str());

		m_progress.set_fraction (double (position) / double (duration));
	}

	void
		Popup::set_playstatus (BmpPlaystatus status)
	{
		if ((status == PLAYSTATUS_STOPPED) || (status == PLAYSTATUS_WAITING))
		{
			m_progress.hide ();
			m_progress.set_text ("");
			m_progress.set_fraction (0.);
		}
		else
		{
			m_progress.show ();
		}
	}

	bool
		Popup::on_expose_event (GdkEventExpose * event)
	{
		Cairo::RefPtr<Cairo::Context> cr = get_window ()->create_cairo_context ();

		int w, h;
		w = get_allocation ().get_width () - 2;
		h = get_allocation ().get_height ();

		// Clear the BG
		cr->set_operator (Cairo::OPERATOR_CLEAR);
		cr->paint ();

		draw_arrow_outline (cr, w, h);

        int offset = 0;

		if (m_image)
			get_window ()->draw_pixbuf (RefPtr<Gdk::GC> (0), m_image, 0, 0, 2, 19, -1, -1, Gdk::RGB_DITHER_MAX, 0, 0);
        else
            offset = 82;

		if (m_source_icon)
		{
              get_window ()->draw_pixbuf (RefPtr<Gdk::GC> (0),
                m_source_icon, 0, 0, popup_width - 10 - m_source_icon->get_width(), arrow_height + 10, -1, -1, Gdk::RGB_DITHER_MAX, 0, 0);
		}

		if (!m_text.empty())
		{
              cr->move_to (94 - offset, 23);
              cr->set_source_rgba (0., 0., 0., .6);
              cr->set_operator (Cairo::OPERATOR_ATOP);
              pango_cairo_show_layout (cr->cobj (), m_layout->gobj ());
		}

		return false;
	}

	void
		Popup::tooltip_mode (bool mode, bool immediate)
	{
		if (G_UNLIKELY(!is_realized())) realize ();

		if (mode)
		{
			m_update_connection.disconnect ();
			m_timer.stop ();
			m_timer.reset ();
			window_set_opacity (get_window (), 1.0);
			m_tooltip_mode = true;
			reposition ();
			Window::show ();
		}
		else if (!mode)
		{
			if (m_tooltip_mode)
			{
				m_tooltip_mode = false;

				if (!immediate)
				{
					m_time_offset = 0.0;
					m_update_connection.disconnect ();
					m_timer.start ();
					m_update_connection = signal_timeout ().connect (sigc::mem_fun (this, &Bmp::Popup::update_frame),
						popup_animation_frame_period_ms);
					disappear ();
				}
				else
				{
					window_set_opacity (get_window (), 1.0);
					hide ();
				}
			}
		}
	}

	void
		Popup::on_realize ()
	{
		Window::on_realize ();

		window_set_opacity (get_window (), 0.0);

		update_mask ();
	}

	void
		Popup::on_show ()
	{
		reposition ();

		Window::on_show ();
	}

	void
		Popup::on_map ()
	{
		Window::on_map ();

		if (!m_tooltip_mode)
		{
			m_time_offset = 0.0;

			m_timer.start ();
			m_update_connection = signal_timeout ().connect (sigc::mem_fun (this, &Bmp::Popup::update_frame),
				popup_animation_frame_period_ms);
		}
	}

	void
		Popup::on_unmap ()
	{
		if (!m_tooltip_mode)
		{
			m_update_connection.disconnect ();
			m_timer.stop ();
			m_timer.reset ();
		}

		Window::on_unmap ();
	}

	void
		Popup::set_source_icon (RefPtr<Gdk::Pixbuf> icon)
	{
		m_source_icon = icon->scale_simple (16, 16, Gdk::INTERP_BILINEAR);
	}

	void
		Popup::metadata_updated (BmpMetadataParseFlags flags)
	{
		if (flags & PARSE_FLAGS_TEXT)
		{
			m_text = ((boost::format ("<big>%s</big>\n<b>%s</b>: <i>%s</i>")
				% (Markup::escape_text (m_metadata.title ? m_metadata.title.get() : std::string()).c_str())
				% (Markup::escape_text (m_metadata.artist ? m_metadata.artist.get() : std::string()).c_str())
				% (Markup::escape_text (m_metadata.album ? m_metadata.album.get() : std::string()).c_str())).str());
			m_layout->set_markup (m_text);
		}

		if ((flags & PARSE_FLAGS_IMAGE) && m_metadata.image_known)
		{
            gtk_widget_set_size_request (GTK_WIDGET(m_progress.gobj()), m_width - 102, 18);
		    gtk_fixed_move (GTK_FIXED(m_fixed.gobj()), GTK_WIDGET(m_progress.gobj()), 94, 78);
			m_image = m_metadata.image->scale_simple (82, 82, Gdk::INTERP_HYPER);
        }
        else
		if ((flags & PARSE_FLAGS_IMAGE) && !m_metadata.image_known)
        {
            gtk_widget_set_size_request (GTK_WIDGET(m_progress.gobj()), m_width - 16, 18);
		    gtk_fixed_move (GTK_FIXED(m_fixed.gobj()), GTK_WIDGET(m_progress.gobj()), 8, 78);
            m_image = RefPixbuf(0);
        }

		queue_update ();
	}

	void
		Popup::queue_update ()
	{
		m_time_offset = get_popup_time_offset (m_fade);
		m_timer.reset ();

		if (is_mapped ())
		{
			reposition ();
			queue_draw ();
		}
	}

	void
		Popup::disappear ()
	{
		double time = m_timer.elapsed () + m_time_offset;
		double disappear_start_time = get_popup_disappear_start_time (m_fade);

		if (time < disappear_start_time)
		{
			m_timer.reset ();
			m_time_offset = disappear_start_time;
		}
	}

	void
		Popup::acquire_widget_info (int & x,
		int & y,
		int & width,
		int & height)
	{
		gdk_flush ();
		while (gtk_events_pending ())
			gtk_main_iteration ();

		gdk_window_get_origin (m_widget->window, &x, &y);

		gdk_flush ();
		while (gtk_events_pending())
			gtk_main_iteration ();

		gdk_window_get_geometry (m_widget->window, NULL, NULL, &width, &height, NULL);
	}

	bool
		Popup::update_frame ()
	{
		double time;

		time = m_timer.elapsed () + m_time_offset;

		if (time < get_popup_end_time (m_fade))
		{
			double alpha = get_popup_alpha_at_time (time, m_fade);

			window_set_opacity (get_window (), alpha);

			return true;
		}
		else
		{
			if (!m_tooltip_mode) hide ();

			return false;
		}
	}

} // namespace Bmp
