/*
 * Copyright 2004-2005 the original author or authors.
 *
 * 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 grails.util;

import groovy.xml.MarkupBuilder;

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

/**
 * <p>Provides support for creating OpenRico AJAX responses.
 *
 * <p>If this builder is used in controllers no views should be configured since
 * content will be written to the HttpServletResponse instance. Also no operations
 * should be performed on the response object prior to passing it to this builder.
 *
 * <p>This builder will set the content type of the response to "text/xml" and will
 * render a correct XML prolog (<?xml version="1.0" encoding="ISO-8859-1"?>).
 *
 * <p>This builder supports no namespaces or hyphens in element and attribute names.
 *
 * <p>Use this builder to write a OpenRico reponse to the client. Sending a simple
 * DIV tag to the client requires this code:
 *
 * <pre>
 * new OpenRicoBuilder(response).ajax { element(id:"personInfo") { div(class:"person") } }
 * </pre>
 *
 * <p>Sending object XML to the client requires this code:
 *
 * <pre>
 * new OpenRicoBuilder(response).ajax { object(id:"formLetterUpdater") {
 *     person(
 *         fullName:"Pat Barnes",
 *         title:"Mr.",
 *         firstName:"Pat",
 *         lastName:"Barnes")
 *     }
 * }
 * </pre>
 *
 * <p>The first call on a OpenRicoBuilder instance should be "ajax", the second either "element" (to return HTML)
 * or object (to return XML) both with a required "id" attribute. Violations against these rules will be
 * reported as exceptions.
 *
 * <p>OpenRico responses can contain multiple element and/or object nodes which is supported by this builder.
 *
 * <p>When using OpenRicoBuilder in controllers you may need to return a null value at the end of a closure to avoid an
 * UnsupporterReturnValueException.
 *
 * @author Steven Devijver
 * @since Jul 5, 2005
 */
public class OpenRicoBuilder extends MarkupBuilder {

    private static final String TEXT_XML = "text/xml";
    private static final String UTF_8 = "UTF-8";
    private static final String AJAX = "ajax";
    private static final String AJAX_RESPONSE = "ajax-response";
    private static final String ELEMENT = "element";
    private static final String OBJECT = "object";
    private static final String RESPONSE = "response";
    private static final String TYPE = "type";
    private static final String ID = "id";
    private static final String OPENRICO = "OpenRicoBuilder: ";
    private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
    private boolean ajaxOnly = true;
    private boolean responseOnly = false;
    private boolean start = true;

    public OpenRicoBuilder(HttpServletResponse response) throws IOException {
        super(response.getWriter());
        response.setContentType(GrailsWebUtil.getContentType(TEXT_XML, UTF_8));
        getPrinter().println(XML_HEADER);
    }

    @Override
    protected Object createNode(Object name) {
        if (responseOnly) {
            throw new IllegalArgumentException(OPENRICO + "only call to [element { }] is allowed with attribute [id], not [" + name + "{ }]!");
        }

        if (start && AJAX.equals(name)) {
            ajaxOnly = false;
            responseOnly = true;
            start = false;
            return super.createNode(AJAX_RESPONSE);
        }

        if (ajaxOnly) {
            throw new IllegalArgumentException(OPENRICO + "only call to [ajax { }] is allowed, not [" + name + " { }]!");
        }

        return super.createNode(name);
    }

    @SuppressWarnings("rawtypes")
    @Override
    protected Object createNode(Object name, Map attributes, Object value) {
        if (checkElementName((String)name, attributes)) {
            return super.createNode(name, attributes, value);
        }

        return RESPONSE;
    }

    @Override
    protected Object createNode(Object name, Object value) {
        if (checkElementName((String)name, null)) {
            return super.createNode(name, value);
        }

        throw new UnsupportedOperationException(OPENRICO + "invalid method call [" + name + "{ }]!");
    }

    @Override
    protected void nodeCompleted(Object parent, Object node) {
        responseOnly = AJAX_RESPONSE.equals(parent) && RESPONSE.equals(node);
        super.nodeCompleted(parent, node);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private boolean checkElementName(String elementName, Map attributes) {
        if (ajaxOnly) {
            throw new IllegalArgumentException(OPENRICO + "only call to [ajax {}] allowed without arguments, not [" + elementName + " { }]!");
        }

        if (responseOnly) {
            responseOnly = false;
            if (!(ELEMENT.equals(elementName) || OBJECT.equals(elementName))) {
                throw new IllegalArgumentException(OPENRICO + "only calls to [element { }] or [object { }] are allowed, not [" + elementName + " { }]!");
            }
            if (attributes == null || attributes.isEmpty()) {
                    throw new IllegalArgumentException(OPENRICO + "call to [" + elementName + " { }] requires id attribute!");
            }
            else if (!(attributes.containsKey(ID) && attributes.get(ID) != null)) {
                    throw new IllegalArgumentException(OPENRICO + "call to [" + elementName + " { }] requires id attribute with value!");
            }
            else if (ELEMENT.equals(elementName)) {
                attributes.put(TYPE, ELEMENT);
                super.createNode(RESPONSE, attributes);
            }
            else if (OBJECT.equals(elementName)) {
                attributes.put(TYPE, OBJECT);
                super.createNode(RESPONSE, attributes);
            }
            return false;
        }
        return true;
    }
}
