// Copyright 2007, 2008, 2009, 2011 The Apache Software Foundation
//
// Licensed 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.tapestry5.corelib.components;

import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.commons.services.TypeCoercer;
import org.apache.tapestry5.http.services.Request;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.Environment;
import org.apache.tapestry5.services.FormSupport;

/**
 * A wrapper component around some number of {@link Radio} components, used to organize the selection and define the
 * property to be edited. Examples of its use are in the {@link Radio} documentation.
 *
 * @tapestrydoc
 */
@Events(EventConstants.VALIDATE)
public class RadioGroup implements Field
{
    /**
     * The property read and updated by the group as a whole.
     */
    @Parameter(required = true, principal = true, autoconnect = true)
    private Object value;

    /**
     * If true, then the field will render out with a disabled attribute (to turn off client-side behavior). Further, a
     * disabled field ignores any value in the request when the form is submitted.
     */
    @Parameter("false")
    private boolean disabled;

    /**
     * The user presentable label for the field. If not provided, a reasonable label is generated from the component's
     * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by
     * converting the actual id to a presentable string (for example, "userId" to "User Id").
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String label;

    /**
     * The id used to generate a page-unique client-side identifier for the component. If a component renders multiple
     * times, a suffix will be appended to the to id to ensure uniqueness. The uniqued value may be accessed via the
     * {@link #getClientId() clientId property}.
     */
    @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
    private String clientId;

    /**
     * A ValueEncoder used to convert server-side objects (provided by the
     * selected Radio componnent's "value" parameter) into unique client-side
     * strings (typically IDs) and back. Note: this parameter may be OMITTED if
     * Tapestry is configured to provide a ValueEncoder automatically for the
     * type of property bound to the "value" parameter.
     */
    @Parameter(required = true, allowNull = false)
    private ValueEncoder encoder;

    /**
     * The object that will perform input validation. The validate binding prefix is generally used to provide this
     * object in a declarative fashion.
     */
    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    @SuppressWarnings("unchecked")
    private FieldValidator<Object> validate;

    @Inject
    private ComponentDefaultProvider defaultProvider;

    @Inject
    private ComponentResources resources;

    @Environmental
    private FormSupport formSupport;

    @Inject
    private Environment environment;

    @Inject
    private Request request;

    @Inject
    private TypeCoercer typeCoercer;

    @Environmental
    private ValidationTracker tracker;

    @Inject
    private FieldValidationSupport fieldValidationSupport;

    private String controlName;

    String defaultLabel()
    {
        return defaultProvider.defaultLabel(resources);
    }

    final ValueEncoder defaultEncoder()
    {
        return defaultProvider.defaultValueEncoder("value", resources);
    }

    private static class Setup implements ComponentAction<RadioGroup>
    {
        private static final long serialVersionUID = -7984673040135949374L;

        private final String controlName;

        Setup(String controlName)
        {
            this.controlName = controlName;
        }

        public void execute(RadioGroup component)
        {
            component.setup(controlName);
        }

        @Override
        public String toString()
        {
            return String.format("RadioGroup.Setup[%s]", controlName);
        }
    }

    private static final ComponentAction<RadioGroup> PROCESS_SUBMISSION = new ComponentAction<RadioGroup>()
    {
        private static final long serialVersionUID = -3857110108918776386L;

        public void execute(RadioGroup component)
        {
            component.processSubmission();
        }

        @Override
        public String toString()
        {
            return "RadioGroup.ProcessSubmission";
        }
    };

    private void setup(String elementName)
    {
        controlName = elementName;
    }

    private void processSubmission()
    {

        if (disabled)
        {
            return;
        }
        String rawValue = request.getParameter(controlName);
        Object convertedValue = encoder.toValue(rawValue);

        tracker.recordInput(this, rawValue);
        try
        {
            if (validate != null)
                fieldValidationSupport.validate(convertedValue, resources, validate);
        }
        catch (ValidationException ex)
        {
            tracker.recordError(this, ex.getMessage());
        }

        value = convertedValue;
    }

    /**
     * Obtains the element name for the group, and stores a {@link RadioContainer} into the {@link Environment} (so that
     * the {@link Radio} components can find it).
     */
    final void setupRender()
    {
        ComponentAction<RadioGroup> action = new Setup(formSupport.allocateControlName(clientId));

        formSupport.storeAndExecute(this, action);

        String submittedValue = tracker.getInput(this);

        final String selectedValue = submittedValue != null ? submittedValue : encoder.toClient(value);

        final Class<?> boundType = resources.getBoundType("value");

        environment.push(RadioContainer.class, new RadioContainer()
        {
            public String getControlName()
            {
                return controlName;
            }

            public boolean isDisabled()
            {
                return disabled;
            }

            private Object getObjectAsCorrectType(Object val)
            {
              if (val != null && boundType != null && !boundType.isAssignableFrom(val.getClass()))
              {
                  return typeCoercer.coerce(val, boundType);
              }
              return val;
            }

            @SuppressWarnings("unchecked")
            public String toClient(Object value)
            {
                return encoder.toClient(getObjectAsCorrectType(value));
            }

            public boolean isSelected(Object value)
            {
                return TapestryInternalUtils.isEqual(encoder.toClient(getObjectAsCorrectType(value)), selectedValue);
            }
        });

        formSupport.store(this, PROCESS_SUBMISSION);
    }

    /**
     * Pops the {@link RadioContainer} off the Environment.
     */
    final void afterRender()
    {
        environment.pop(RadioContainer.class);
    }

    public String getControlName()
    {
        return controlName;
    }

    public String getLabel()
    {
        return label;
    }

    public boolean isDisabled()
    {
        return disabled;
    }

    /**
     * Returns null; the radio group does not render as a tag and so doesn't have an id to share. RadioGroup implements
     * {@link org.apache.tapestry5.Field} only so it can interact with the
     * {@link org.apache.tapestry5.ValidationTracker}.
     *
     * @return null
     */
    public String getClientId()
    {
        return null;
    }

    public boolean isRequired()
    {
        return validate.isRequired();
    }
}
