/*
 * 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.model;

import java.util.Locale;

import org.danekja.java.util.function.serializable.SerializableSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Model that makes working with detachable models a breeze. LoadableDetachableModel holds a
 * temporary, transient model object, that is set when {@link #getObject()} is called by calling
 * abstract method 'load', and that will be reset/ set to null on {@link #detach()}.
 * 
 * A usage example:
 * 
 * <pre>
 * LoadableDetachableModel venueListModel = new LoadableDetachableModel()
 * {
 * 	protected Object load()
 * 	{
 * 		return getVenueDao().findVenues();
 * 	}
 * };
 * </pre>
 * 
 * <p>
 * Though you can override methods {@link #onAttach()} and {@link #onDetach()} for additional
 * attach/ detach behavior, the point of this class is to hide as much of the attaching/ detaching
 * as possible. So you should rarely need to override those methods, if ever.
 * </p>
 * 
 * @author Eelco Hillenius
 * @author Igor Vaynberg
 * 
 * @param <T>
 *            The Model Object type
 */
public abstract class LoadableDetachableModel<T> implements IModel<T>
{
	private static final long serialVersionUID = 1L;

	private static final Logger log = LoggerFactory.getLogger(LoadableDetachableModel.class);

	/** Internal state of the LoadableDetachableModel. */
	private enum InternalState {
		DETACHED, ATTACHING, ATTACHED;

		@Override
		public String toString()
		{
			return name().toLowerCase(Locale.ROOT);
		}
	}

	/** Keeps track of whether this model is attached or detached */
	private transient InternalState state = InternalState.DETACHED;

	/** temporary, transient object. */
	private transient T transientModelObject;

	/**
	 * Default constructor, constructs the model in detached state with no data associated with the
	 * model.
	 */
	public LoadableDetachableModel()
	{
	}

	/**
	 * This constructor is used if you already have the object retrieved and want to wrap it with a
	 * detachable model. Constructs the model in attached state. Calls to {@link #getObject()} will
	 * return {@code object} until {@link #detach()} is called.
	 * 
	 * @param object
	 *            retrieved instance of the detachable object
	 */
	public LoadableDetachableModel(T object)
	{
		this.transientModelObject = object;
		state = InternalState.ATTACHED;
	}

	@Override
	public void detach()
	{
		// even if LDM is in partial attached state (ATTACHING) it should be detached
		if (state != null && state != InternalState.DETACHED)
		{
			try
			{
				onDetach();
			}
			finally
			{
				state = InternalState.DETACHED;
				transientModelObject = null;

				log.debug("removed transient object for '{}'", this);
			}
		}
	}

	@Override
	public final T getObject()
	{
		if (state == null || state == InternalState.DETACHED)
		{
			// prevent infinite attachment loops
			state = InternalState.ATTACHING;

			transientModelObject = load();

			if (log.isDebugEnabled())
			{
				log.debug("loaded transient object '{}' for '{}'", transientModelObject, this);
			}

			state = InternalState.ATTACHED;
			onAttach();
		}
		return transientModelObject;
	}

	/**
	 * Gets the attached status of this model instance
	 * 
	 * @return true if the model is attached, false otherwise
	 */
	public final boolean isAttached()
	{
		return state == InternalState.ATTACHED;
	}

	@Override
	public String toString()
	{
		StringBuilder sb = new StringBuilder(super.toString());
		sb.append(":attached=")
			.append(isAttached())
			.append(":tempModelObject=[")
			.append(this.transientModelObject)
			.append(']');
		return sb.toString();
	}

	/**
	 * Loads and returns the (temporary) model object.
	 * 
	 * @return the (temporary) model object
	 */
	protected abstract T load();

	/**
	 * Attaches to the current request. Implement this method with custom behavior, such as loading
	 * the model object.
	 */
	protected void onAttach()
	{
	}

	/**
	 * Detaches from the current request. Implement this method with custom behavior, such as
	 * setting the model object to null.
	 */
	protected void onDetach()
	{
	}

	/**
	 * Manually loads the model with the specified object. Subsequent calls to {@link #getObject()}
	 * will return {@code object} until {@link #detach()} is called.
	 * 
	 * @param object
	 *            The object to set into the model
	 */
	@Override
	public void setObject(final T object)
	{
		state = InternalState.ATTACHED;
		transientModelObject = object;
	}
	
	/**
	 * Create a {@link LoadableDetachableModel} for the given supplier.
	 *
	 * @param <T>
	 * @param getter Used for the getObject() method.
	 * @return the model
	 */
	public static <T> LoadableDetachableModel<T> of(SerializableSupplier<T> getter)
	{
		return new LoadableDetachableModel<T>()
		{
			private static final long serialVersionUID = 1L;

			@Override
			protected T load()
			{
				return getter.get();
			}
		};
	}
}