/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.extensions.rating;

import java.util.Optional;

import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
import org.apache.wicket.markup.head.CssHeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.list.Loop;
import org.apache.wicket.markup.html.list.LoopItem;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.handler.resource.ResourceReferenceRequestHandler;
import org.apache.wicket.request.resource.CssResourceReference;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.request.resource.ResourceReference;

/**
 * Rating component that generates a number of stars where a user can click on to rate something.
 * Subclasses should implement {@link #onRated(int, Optional)} to provide the calculation
 * of the rating, and {@link #onIsStarActive(int)} to indicate whether to render an active star or
 * an inactive star.
 * <p>
 * Active stars are the stars that show the rating, inactive stars are the left overs. E.G. a rating
 * of 3.4 on a scale of 5 stars will render 3 active stars, and 2 inactive stars (provided that the
 * {@link #onIsStarActive(int)} returns <code>true</code> for each of the first three stars).
 * <p>
 * Use this component in the following way:
 * 
 * <pre>
 * add(new RatingPanel(&quot;rating&quot;, new PropertyModel(rating, &quot;rating&quot;), 5)
 * {
 * 	protected boolean onIsStarActive(int star)
 * 	{
 * 		return rating.isActive(star);
 * 	}
 * 
 * 	protected void onRated(int rating, AjaxRequestTarget target)
 * 	{
 * 		rating1.addRating(rating);
 * 	}
 * });
 * </pre>
 * 
 * The user of this component is responsible for creating a model that supplies a Double (or Float)
 * value for the rating message, however the rating panel doesn't necessarily have to contain a
 * float or number rating value.
 * <p>
 * Though not obligatory, you could also supply a value for the number of votes cast, which allows
 * the component to render a more complete message in the rating label.
 * 
 * <h2>Customizing the rating value and label</h2>
 * To customize the rating value, one should override the
 * {@link #newRatingLabel(String, IModel, IModel)} method and create another label instead, based on
 * the provided models. If you do so, and use another system of rating than returning a Float or
 * Double, then you should also customize the rating resource bundle to reflect your message. The
 * default resource bundle assumes a numeric value for the rating.
 * 
 * <h2>Resource bundle</h2>
 * This component uses two types of messages: rating.simple and rating.complete. The first message
 * is used when no model is given for the number of cast votes. The complete message shows the text
 * 'Rating xx.yy from zz votes'.
 * 
 * <pre>
 *       rating.simple=Rated {0,number,#.#}
 *       rating.complete=Rated {0,number,#.#} from {1,number,#} votes
 * </pre>
 * 
 * <h2>Customizing the star images</h2>
 * To customize the images shown, override the {@link #getActiveStarUrl(int)} and
 * {@link #getInactiveStarUrl(int)} methods. Using the iteration parameter it is possible to use a
 * different image for each star, creating a fade effect or something similar.
 * 
 * @author Martijn Dashorst
 */
public abstract class RatingPanel extends Panel
{
	/**
	 * Renders the stars and the links necessary for rating.
	 */
	private final class RatingStarBar extends Loop
	{
		/** For serialization. */
		private static final long serialVersionUID = 1L;

		private RatingStarBar(final String id, final IModel<Integer> model)
		{
			super(id, model);
		}

		@Override
		protected void populateItem(final LoopItem item)
		{
			// Use an AjaxFallbackLink for rating to make voting work even
			// without Ajax.
			AjaxFallbackLink<Void> link = new AjaxFallbackLink<Void>("link")
			{
				private static final long serialVersionUID = 1L;

				@Override
				public void onClick(Optional<AjaxRequestTarget> targetOptional)
				{
					LoopItem item = (LoopItem)getParent();

					// adjust the rating, and provide the target to the subclass
					// of our rating component, so other components can also get
					// updated in case of an AJAX event.

					onRated(item.getIndex() + 1, targetOptional);

					// if we process an AJAX event, update this panel
					targetOptional.ifPresent(target -> target.add(RatingPanel.this.get("rater")));

				}

				@Override
				public boolean isEnabled()
				{
					return !hasVoted.getObject();
				}
			};

			int iteration = item.getIndex();

			// add the star image, which is either active (highlighted) or
			// inactive (no star)
			link.add(new WebMarkupContainer("star").add(AttributeModifier.replace("src",
				(onIsStarActive(iteration) ? getActiveStarUrl(iteration)
					: getInactiveStarUrl(iteration)))));
			item.add(link);
		}
	}

	/** For serialization. */
	private static final long serialVersionUID = 1L;

	/**
	 * Star image for no selected star
	 */
	public static final ResourceReference STAR0 = new PackageResourceReference(RatingPanel.class,
		"star0.gif");

	/**
	 * Star image for selected star
	 */
	public static final ResourceReference STAR1 = new PackageResourceReference(RatingPanel.class,
		"star1.gif");

	/**
	 * The number of stars that need to be shown, should result in an Integer object.
	 */
	private IModel<Integer> nrOfStars = new Model<>(5);

	/**
	 * The number of votes that have been cast, should result in an Integer object.
	 */
	private final IModel<Integer> nrOfVotes;

	/**
	 * The flag on whether the current user has voted already.
	 */
	private final IModel<Boolean> hasVoted;

	/**
	 * Handle to the rating label to set the visibility.
	 */
	private Component ratingLabel;

	private final boolean addDefaultCssStyle;

	/**
	 * Constructs a rating component with 5 stars, using a compound property model as its model to
	 * retrieve the rating.
	 * 
	 * @param id
	 *            the component id.
	 */
	public RatingPanel(final String id)
	{
		this(id, null, 5, true);
	}

	/**
	 * Constructs a rating component with 5 stars, using the rating for retrieving the rating.
	 * 
	 * @param id
	 *            the component id
	 * @param rating
	 *            the model to get the rating
	 */
	public RatingPanel(final String id, final IModel<? extends Number> rating)
	{
		this(id, rating, new Model<Integer>(5), null, new Model<>(Boolean.FALSE), true);
	}

	/**
	 * Constructs a rating component with nrOfStars stars, using a compound property model as its
	 * model to retrieve the rating.
	 * 
	 * @param id
	 *            the component id
	 * @param nrOfStars
	 *            the number of stars to display
	 */
	public RatingPanel(final String id, final int nrOfStars)
	{
		this(id, null, nrOfStars, true);
	}

	/**
	 * Constructs a rating component with nrOfStars stars, using the rating for retrieving the
	 * rating.
	 * 
	 * @param id
	 *            the component id
	 * @param rating
	 *            the model to get the rating
	 * @param nrOfStars
	 *            the number of stars to display
	 * @param addDefaultCssStyle
	 *            should this component render its own default CSS style?
	 */
	public RatingPanel(final String id, final IModel<? extends Number> rating, final int nrOfStars,
		final boolean addDefaultCssStyle)
	{
		this(id, rating, new Model<Integer>(nrOfStars), null, new Model<Boolean>(Boolean.FALSE),
			addDefaultCssStyle);
	}

	/**
	 * Constructs a rating panel with nrOfStars stars, where the rating model is used to retrieve
	 * the rating, the nrOfVotes model to retrieve the number of casted votes. This panel doens't
	 * keep track of whether the user has already voted.
	 * 
	 * @param id
	 *            the component id
	 * @param rating
	 *            the model to get the rating
	 * @param nrOfStars
	 *            the number of stars to display
	 * @param nrOfVotes
	 *            the number of cast votes
	 * @param addDefaultCssStyle
	 *            should this component render its own default CSS style?
	 */
	public RatingPanel(final String id, final IModel<? extends Number> rating, final int nrOfStars,
		final IModel<Integer> nrOfVotes, final boolean addDefaultCssStyle)
	{
		this(id, rating, new Model<Integer>(nrOfStars), nrOfVotes,
			new Model<>(Boolean.FALSE), addDefaultCssStyle);
	}

	/**
	 * Constructs a rating panel with nrOfStars stars, where the rating model is used to retrieve
	 * the rating, the nrOfVotes model used to retrieve the number of votes cast and the hasVoted
	 * model to retrieve whether the user already had cast a vote.
	 * 
	 * @param id
	 *            the component id.
	 * @param rating
	 *            the (calculated) rating, i.e. 3.4
	 * @param nrOfStars
	 *            the number of stars to display
	 * @param nrOfVotes
	 *            the number of cast votes
	 * @param hasVoted
	 *            has the user already voted?
	 * @param addDefaultCssStyle
	 *            should this component render its own default CSS style?
	 */
	public RatingPanel(final String id, final IModel<? extends Number> rating,
		final IModel<Integer> nrOfStars, final IModel<Integer> nrOfVotes,
		final IModel<Boolean> hasVoted, final boolean addDefaultCssStyle)
	{
		super(id, rating);
		this.addDefaultCssStyle = addDefaultCssStyle;

		this.nrOfStars = wrap(nrOfStars);
		this.nrOfVotes = wrap(nrOfVotes);
		this.hasVoted = wrap(hasVoted);

		WebMarkupContainer rater = new WebMarkupContainer("rater");
		rater.add(newRatingStarBar("element", this.nrOfStars));

		// add the text label for the message 'Rating 4.5 out of 25 votes'
		rater.add(ratingLabel = newRatingLabel("rating", wrap(rating), this.nrOfVotes));

		// set auto generation of the markup id on, such that ajax calls work.
		rater.setOutputMarkupId(true);

		add(rater);

		// don't render the outer tags in the target document, just the div that
		// is inside the panel.
		setRenderBodyOnly(true);
	}

	@Override
	public void renderHead(final IHeaderResponse response)
	{
		super.renderHead(response);
		if (addDefaultCssStyle)
		{
			response.render(CssHeaderItem.forReference(new CssResourceReference(
				RatingPanel.class, "RatingPanel.css")));
		}

	}

	/**
	 * Creates a new bar filled with stars to click on.
	 * 
	 * @param id
	 *            the bar id
	 * @param nrOfStars
	 *            the number of stars to generate
	 * @return the bar with rating stars
	 */
	protected Component newRatingStarBar(final String id, final IModel<Integer> nrOfStars)
	{
		return new RatingStarBar(id, nrOfStars);
	}

	/**
	 * Creates a new rating label, showing a message like 'Rated 5.4 from 53 votes'.
	 * 
	 * @param id
	 *            the id of the label
	 * @param rating
	 *            the model containing the rating
	 * @param nrOfVotes
	 *            the model containing the number of votes (may be null)
	 * @return the label component showing the message.
	 */
	protected Component newRatingLabel(final String id, final IModel<? extends Number> rating,
		final IModel<Integer> nrOfVotes)
	{
		IModel<String> model;
		if (nrOfVotes == null)
		{
			Object[] parameters = new Object[] { rating };
			model = new StringResourceModel("rating.simple", this).setParameters(parameters);
		}
		else
		{
			Object[] parameters = new Object[] { rating, nrOfVotes };
			model = new StringResourceModel("rating.complete", this).setParameters(parameters);
		}
		return new Label(id, model);
	}

	/**
	 * Returns the url pointing to the image of active stars, is used to set the URL for the image
	 * of an active star. Override this method to provide your own images.
	 * 
	 * @param iteration
	 *            the sequence number of the star
	 * @return the url pointing to the image for active stars.
	 */
	protected String getActiveStarUrl(final int iteration)
	{
		IRequestHandler handler = new ResourceReferenceRequestHandler(STAR1);
		return getRequestCycle().urlFor(handler).toString();
	}

	/**
	 * Returns the url pointing to the image of inactive stars, is used to set the URL for the image
	 * of an inactive star. Override this method to provide your own images.
	 * 
	 * @param iteration
	 *            the sequence number of the star
	 * @return the url pointing to the image for inactive stars.
	 */
	protected String getInactiveStarUrl(final int iteration)
	{
		IRequestHandler handler = new ResourceReferenceRequestHandler(STAR0);
		return getRequestCycle().urlFor(handler).toString();
	}

	/**
	 * Sets the visibility of the rating label.
	 * 
	 * @param visible
	 *            true when the label should be visible
	 * @return this for chaining.
	 */
	public RatingPanel setRatingLabelVisible(final boolean visible)
	{
		ratingLabel.setVisible(visible);
		return this;
	}

	/**
	 * Returns <code>true</code> when the star identified by its sequence number should be shown as
	 * active.
	 * 
	 * @param star
	 *            the sequence number of the star (ranging from 0 to nrOfStars)
	 * @return <code>true</code> when the star should be rendered as active
	 */
	protected abstract boolean onIsStarActive(int star);

	/**
	 * Notification of a click on a rating star. Add your own components to the request target when
	 * you want to have them updated in the Ajax request. <strong>NB</strong> the target may be null
	 * when the click isn't handled using AJAX, but using a fallback scenario.
	 *  @param rating
	 *            the number of the star that is clicked, ranging from 1 to nrOfStars
	 * @param target
	 */
	protected abstract void onRated(int rating, Optional<AjaxRequestTarget> target);
}
