/**
 * 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.cxf.js.rhino;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Endpoint;

import org.w3c.dom.Node;

import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Wrapper;


public abstract class AbstractDOMProvider {
    public static class JSDOMProviderException extends Exception {
        public JSDOMProviderException(String reason) {
            super(reason);
        }
    }

    public static final String NO_WSDL_LOCATION = "no wsdlLocation property found";
    public static final String NO_SERVICE_NAME = "no serviceName property found";
    public static final String NO_PORT_NAME = "no portName property found";
    public static final String NO_TGT_NAMESPACE = "no targetNamespace property found";
    public static final String NO_INVOKE = "no invoke property found";
    public static final String ILLEGAL_INVOKE_TYPE = "invoke property is not a Function type";
    public static final String NO_EP_ADDR = "no endpoint address specified";

    private Scriptable scriptScope;
    private Scriptable webSvcProviderVar;
    private String epAddress;
    private boolean isBaseAddr;
    private boolean isE4X;
    private Function invokeFunc;

    protected AbstractDOMProvider(Scriptable scope,
                                  Scriptable wspVar, String epAddr,
                                  boolean isBase, boolean e4x) {
        scriptScope = scope;
        webSvcProviderVar = wspVar;
        epAddress = epAddr;
        isBaseAddr = isBase;
        isE4X = e4x;
    }

    public void publish() throws Exception {
        String addr = epAddress;
        String wsdlLoc = null;
        String svcNm = null;
        String portNm = null;
        String tgtNmspc = null;
        String binding = null;
        Object obj = webSvcProviderVar.get("wsdlLocation", webSvcProviderVar);
        if (obj == Scriptable.NOT_FOUND) {
            throw new JSDOMProviderException(NO_WSDL_LOCATION);
        }
        if (obj instanceof String) {
            wsdlLoc = (String)obj;
        }
        obj = webSvcProviderVar.get("serviceName", webSvcProviderVar);
        if (obj == Scriptable.NOT_FOUND) {
            throw new JSDOMProviderException(NO_SERVICE_NAME);
        }
        if (obj instanceof String) {
            svcNm = (String)obj;
        }
        obj = webSvcProviderVar.get("portName", webSvcProviderVar);
        if (obj == Scriptable.NOT_FOUND) {
            throw new JSDOMProviderException(NO_PORT_NAME);
        }
        if (obj instanceof String) {
            portNm = (String)obj;
        }
        obj = webSvcProviderVar.get("targetNamespace", webSvcProviderVar);
        if (obj == Scriptable.NOT_FOUND) {
            throw new JSDOMProviderException(NO_TGT_NAMESPACE);
        }
        if (obj instanceof String) {
            tgtNmspc = (String)obj;
        }
        if (addr == null) {
            obj = webSvcProviderVar.get("EndpointAddress", scriptScope);
            if (obj != Scriptable.NOT_FOUND && obj instanceof String) {
                addr = (String)obj;
                isBaseAddr = false;
            }
        }
        if (addr == null) {
            throw new JSDOMProviderException(NO_EP_ADDR);
        }
        if (isBaseAddr) {
            if (addr.endsWith("/")) {
                addr += portNm;
            } else {
                addr = addr + "/" + portNm;
            }
        }
        obj = webSvcProviderVar.get("BindingType", scriptScope);
        if (obj != Scriptable.NOT_FOUND && obj instanceof String) {
            binding = (String)obj;
        }
        obj = webSvcProviderVar.get("invoke", webSvcProviderVar);
        if (obj == Scriptable.NOT_FOUND) {
            throw new JSDOMProviderException(NO_INVOKE);
        }
        if (obj instanceof Function) {
            invokeFunc = (Function)obj;
        } else {
            throw new JSDOMProviderException(ILLEGAL_INVOKE_TYPE);
        }
        Endpoint ep = Endpoint.create(binding, this);
        List<Source> metadata = new ArrayList<Source>();
        metadata.add(new StreamSource(wsdlLoc));
        ep.setMetadata(metadata);
        Map<String, Object> props = new HashMap<String, Object>();
        props.put(Endpoint.WSDL_SERVICE, new QName(tgtNmspc, svcNm));
        props.put(Endpoint.WSDL_PORT, new QName(tgtNmspc, portNm));
        ep.setProperties(props);
        ep.publish(addr);
    }

    public DOMSource invoke(DOMSource request) {
        DOMSource response = new DOMSource();
        Context cx = Context.enter();
        try {
            Scriptable scope = cx.newObject(scriptScope);
            scope.setPrototype(scriptScope);
            scope.setParentScope(null);
            Node node = request.getNode();
            Object inDoc = null;
            if (isE4X) {
                try {
                    XmlObject xo = XmlObject.Factory.parse(node);
                    XmlCursor cursor = xo.newCursor();
                    // strip comments out, as xmlbeans doesn't
                    // seem to like them
                    do {
                        if (cursor.isComment()) {
                            cursor.removeXml();
                        }
                    } while (cursor.toNextToken() != XmlCursor.TokenType.NONE);
                    cursor.dispose();           
                    inDoc = Context.toObject(xo, scope);
                    Object[] args = {inDoc};
                    inDoc = cx.newObject(scriptScope, "XML", args);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            } else {
                inDoc = Context.toObject(node, scope);
            }
            Object[] args = {inDoc};
            Object jsResp = invokeFunc.call(cx, scope, scope, args);
            if (isE4X) {
                // need to check return type and throw exception
                // if wrong type
                Scriptable s = (Scriptable)jsResp;
                Object out = ScriptableObject.callMethod(s,
                                                         "getXmlObject",
                                                         Context.emptyArgs);
                Wrapper wrapped = (Wrapper)out;
                XmlObject xml = (XmlObject)wrapped.unwrap();
                node = xml.getDomNode();
                response.setNode(node.getOwnerDocument());
            } else {
                if (jsResp instanceof Wrapper) {
                    jsResp = ((Wrapper)jsResp).unwrap();
                }
                if (jsResp instanceof Node) {
                    node = (Node)jsResp;
                    response.setNode(node);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            Context.exit();
        }
        return response;
    }
}
