/**
 * 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.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.xml.ws.Service;
import org.apache.cxf.common.logging.LogUtils;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;

public class ProviderFactory {
    public static final String ILLEGAL_SVCMD_MODE = ": unknown ServiceMode: ";
    public static final String ILLEGAL_SVCMD_TYPE = ": ServiceMode value must be of type string";
    public static final String NO_SUCH_FILE = ": file does not exist";
    public static final String NO_PROVIDER = ": file contains no WebServiceProviders";

    private static final Logger LOG = LogUtils.getL7dLogger(ProviderFactory.class);

    private String epAddress;
    private boolean isBaseAddr;
    private List<AbstractDOMProvider> providers = new CopyOnWriteArrayList<>();

    static {
        ContextFactory.initGlobal(new RhinoContextFactory());
    }

    public ProviderFactory(String baseAddr) {
        epAddress = baseAddr;
        isBaseAddr = true;
    }

    public ProviderFactory() {
        // complete
    }

    public void createAndPublish(File f, String epAddr, boolean isBase) throws Exception {
        publishImpl(f, epAddr, isBase);
    }

    public synchronized void createAndPublish(File f) throws Exception {
        publishImpl(f, epAddress, isBaseAddr);
    }

    private void publishImpl(File f, String epAddr, boolean isBase) throws Exception {
        if (!f.exists()) {
            throw new Exception(f.getPath() + NO_SUCH_FILE);
        }
        boolean isE4X = f.getName().endsWith(".jsx");
        StringBuilder sb = new StringBuilder();
        try (BufferedReader bufrd = new BufferedReader(new FileReader(f))) {
            String line;
            while (null != (line = bufrd.readLine())) {
                sb.append(line).append('\n');
            }
        }

        String scriptStr = sb.toString();

        Context cx = ContextFactory.getGlobal().enterContext();
        boolean providerFound = false;
        try {
            Scriptable scriptScope = cx.initStandardObjects(null, true);
            Object[] ids = compileScript(cx, scriptStr, scriptScope, f);
            if (ids.length > 0) {
                Service.Mode mode = Service.Mode.PAYLOAD;
                for (Object idObj : ids) {
                    if (!(idObj instanceof String)) {
                        continue;
                    }
                    String id = (String)idObj;
                    if (!id.startsWith("WebServiceProvider")) {
                        continue;
                    }
                    Object obj = scriptScope.get(id, scriptScope);
                    if (!(obj instanceof Scriptable)) {
                        continue;
                    }
                    Scriptable wspVar = (Scriptable)obj;
                    providerFound = true;
                    obj = wspVar.get("ServiceMode", wspVar);
                    if (obj != Scriptable.NOT_FOUND) {
                        if (obj instanceof String) {
                            String value = (String)obj;
                            if ("PAYLOAD".equalsIgnoreCase(value)) {
                                mode = Service.Mode.PAYLOAD;
                            } else if ("MESSAGE".equalsIgnoreCase(value)) {
                                mode = Service.Mode.MESSAGE;
                            } else {
                                throw new Exception(f.getPath() + ILLEGAL_SVCMD_MODE + value);
                            }
                        } else {
                            throw new Exception(f.getPath() + ILLEGAL_SVCMD_TYPE);
                        }
                    }
                    AbstractDOMProvider provider
                        = createProvider(mode, scriptScope, wspVar,
                                         epAddr, isBase, isE4X);
                    try {
                        provider.publish();
                        providers.add(provider);
                    } catch (AbstractDOMProvider.JSDOMProviderException ex) {
                        StringBuilder msg = new StringBuilder(f.getPath());
                        msg.append(": ").append(ex.getMessage());
                        throw new Exception(msg.toString());
                    }
                }
            }
        } finally {
            Context.exit();
        }
        if (!providerFound) {
            throw new Exception(f.getPath() + NO_PROVIDER);
        }
    }

    public void stop() {
        while (!providers.isEmpty()) {
            AbstractDOMProvider p = providers.remove(0);
            p.stop();
        }
    }

    protected AbstractDOMProvider createProvider(Service.Mode mode, Scriptable scope,
                                                 Scriptable wsp, String epAddr,
                                                 boolean isBase, boolean e4x) throws Exception {
        if (LOG.isLoggable(Level.FINE)) {
            String modestr = (mode == Service.Mode.PAYLOAD) ? "payload" : "message";
            String type = e4x ? "E4X" : "JavaScript";
            String base = isBase ? "base " : "";
            StringBuilder msg = new StringBuilder("creating a ");
            msg.append(modestr)
                .append(' ')
                .append(type)
                .append(" provider for ")
                .append(base)
                .append("address ")
                .append(epAddr);
            LOG.log(Level.FINE, msg.toString());
        }
        AbstractDOMProvider provider = null;
        if (mode == Service.Mode.PAYLOAD) {
            provider = new DOMPayloadProvider(scope, wsp, epAddr, isBase, e4x);
        } else if (mode == Service.Mode.MESSAGE) {
            provider = new DOMMessageProvider(scope, wsp, epAddr, isBase, e4x);
        }
        return provider;
    }

    private Object[] compileScript(Context cx, String scriptStr, Scriptable scriptScope, File f) {
        int opt = cx.getOptimizationLevel();
        cx.setOptimizationLevel(-1);
        Script script = cx.compileString(scriptStr, f.getName(), 1, null);
        script.exec(cx, scriptScope);
        Object[] ids = scriptScope.getIds();
        cx.setOptimizationLevel(opt);
        script = cx.compileString(scriptStr, f.getName(), 1, null);
        script.exec(cx, scriptScope);
        return ids;
    }

    static class RhinoContextFactory extends ContextFactory {
        public boolean hasFeature(Context cx, int feature) {
            if (feature == Context.FEATURE_DYNAMIC_SCOPE) {
                return true;
            }
            return super.hasFeature(cx, feature);
        }
    }
}
