/***
 * Excerpted from "Domain-Driven Design Using Naked Objects",
 * published by The Pragmatic Bookshelf.
 * Copyrights apply to this code. It may not be used to create training material, 
 * courses, books, articles, and the like. Contact us if you are in doubt.
 * We make no guarantees that this code is fit for any purpose. 
 * Visit http://www.pragmaticprogrammer.com/titles/dhnako for more book information.
***/
package com.pragprog.dhnako.carserv.wicket.app;

import java.io.Serializable;
import java.util.Map;

import org.apache.wicket.MarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.validation.FormComponentFeedbackBorder;
import org.apache.wicket.validation.IValidatable;
import org.apache.wicket.validation.validator.AbstractValidator;
import org.nakedobjects.metamodel.adapter.NakedObject;
import org.nakedobjects.metamodel.authentication.AuthenticationSession;
import org.nakedobjects.metamodel.consent.Consent;
import org.nakedobjects.metamodel.spec.NakedObjectSpecification;
import org.nakedobjects.metamodel.spec.feature.NakedObjectAssociation;
import org.nakedobjects.metamodel.spec.feature.OneToOneAssociation;
import org.nakedobjects.metamodel.specloader.SpecificationLoader;
import org.nakedobjects.runtime.context.NakedObjectsContext;
import org.nakedobjects.runtime.persistence.PersistenceSession;
import org.nakedobjects.runtime.persistence.adaptermanager.AdapterManager;


public class NakedObjectsPropertyAdapter implements Serializable {
	
	private static final long serialVersionUID = 1L;
	
	private final FormComponent formComponent;
	private final MarkupContainer border;
	private Label label;
	
	public NakedObjectsPropertyAdapter(final FormComponent formComponent) {
		this.formComponent = formComponent;
		this.border = newBorderFor(formComponent);
	}

	private MarkupContainer newBorderFor(final FormComponent formComponent) {
		return new FormComponentFeedbackBorder(borderIdFor(formComponent)).add(formComponent);
	}
	
	private String borderIdFor(final FormComponent formComponent) {
		return formComponent.getId() + "Border";
	}

	////////////////////////////////////////////////////////
	// Form Components
	////////////////////////////////////////////////////////

	public FormComponent getFormComponent() {
		return formComponent;
	}

	public MarkupContainer getBorderedFormComponent() {
		return border;
	}
	
	public Label getLabel() {
		ensureAddedToForm();
		return label;
	}

	////////////////////////////////////////////////////////
	// Parent, Model
	////////////////////////////////////////////////////////

	/**
	 * Only non-<tt>null</tt> after {@link #addToFormAndConfigureRules(Form)}.
	 */
	private MarkupContainer getParent() {
		return getBorderedFormComponent().getParent();
	}

	private Object getModelObject() {
		ensureAddedToForm();
		return getParent().getModelObject();
	}

	private Class<?> getModelClass() {
		ensureAddedToForm();
		ModelForm modelForm = (ModelForm) getParent();
		return modelForm.getModelClass();
	}

	private NakedObject getModelAdapter() {
		Object modelObject = getModelObject();
		return getAdapterManager().adapterFor(modelObject);
	}

	private NakedObjectAssociation getAssociation() {
		NakedObjectSpecification noSpec = getSpecificationLoader().loadSpecification(getModelClass());
		return noSpec.getAssociation(getFormComponent().getId());
	}

	private OneToOneAssociation getOneToOneAssociation() {
		return (OneToOneAssociation) getAssociation();
	}


	////////////////////////////////////////////////////////
	// Adding to form
	////////////////////////////////////////////////////////

	public void addToFormAndConfigureRules(Form form) {
		if (!(form instanceof ModelForm)) {
			throw new IllegalArgumentException("Must be a ModelForm");
		}
		form.add(getBorderedFormComponent());
		
		this.label = newLabelForFormComponent(formComponent, getAssociation().getName() + ":");
		form.add(getLabel());
		configureRules();
	}

	private Label newLabelForFormComponent(FormComponent formComponent, String labelStr) {
		return new Label(labelIdFor(formComponent), labelStr);
	}

	private String labelIdFor(FormComponent formComponent) {
		return formComponent.getId() + "Label";
	}

	private void ensureAddedToForm() {
		if (getParent() == null) {
			throw new IllegalStateException("Only available after added to form.");
		}
	}


	////////////////////////////////////////////////////////
	// Rules
	////////////////////////////////////////////////////////

	private void configureRules() {
		setVisible(visibilityConsent().isAllowed());
		setEnabled(usabilityConsent().isAllowed());
		
		final class NakedObjectsPropertyValidator extends AbstractValidator {
			private static final long serialVersionUID = 1L;
			
			private StringBuilder reasonBuf = new StringBuilder();

			@Override
			protected void onValidate(IValidatable validatable) {
				Object proposedValue = validatable.getValue();
				Consent validityConsent = validityConsent(proposedValue);
				if (validityConsent.isVetoed()) {
					String reason = validityConsent.getReason();
					reasonBuf.setLength(0); // clear
					this.reasonBuf.append(reason);
					error(validatable);
				}
			}

			@Override
			protected String resourceKey() {
				return "NakedObjectsPropertyValidator";
			}
			
			
			@SuppressWarnings("unchecked")
			@Override
			protected Map variablesMap(IValidatable validatable) {
				Map map = super.variablesMap(validatable);
				map.put("reason", reasonBuf);
				return map;
			}
		}
		
		getFormComponent().add(new NakedObjectsPropertyValidator());
	}

	private void setVisible(boolean visible) {
		label.setVisible(visible);
		formComponent.setVisible(visible);
	}

	private void setEnabled(boolean enabled) {
		label.setEnabled(enabled);
		formComponent.setEnabled(enabled);
	}

	private Consent visibilityConsent() {
		return getAssociation().isVisible(
				getAuthenticationSession(), getModelAdapter());
	}

	private Consent usabilityConsent() {
		return getAssociation().isUsable(
				getAuthenticationSession(), getModelAdapter());
	}

	private Consent validityConsent(
			final Object proposedValue) {
		return getOneToOneAssociation().isAssociationValid(getModelAdapter(), adapterFor(proposedValue));
	}


	////////////////////////////////////////////////////////
	// Dependencies (from NakedObjectsContext)
	////////////////////////////////////////////////////////

	private static NakedObject adapterFor(Object value) {
		return getAdapterManager().adapterFor(value);
	}


	private static SpecificationLoader getSpecificationLoader() {
		return NakedObjectsContext.getSpecificationLoader();
	}

	private static AdapterManager getAdapterManager() {
		return getPersistenceSession().getAdapterManager();
	}

	private static PersistenceSession getPersistenceSession() {
		return NakedObjectsContext.getPersistenceSession();
	}

	private static AuthenticationSession getAuthenticationSession() {
		return NakedObjectsContext.getAuthenticationSession();
	}

}
