/**
 * 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.binding.corba.utils;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import org.apache.cxf.binding.corba.CorbaBindingException;
import org.apache.cxf.common.logging.LogUtils;
import org.omg.CORBA.ORB;

public final class CorbaBindingHelper {

    private static final Logger LOG = LogUtils.getL7dLogger(CorbaBindingHelper.class);
    private static Map<String, ORB> orbList = new HashMap<>();
    private static Map<String, Integer> orbUseCount = new HashMap<>();
    private static ORB defaultORB;

    private CorbaBindingHelper() {
        //utility class
    }

    public static synchronized ORB getDefaultORB(OrbConfig config) {
        if (defaultORB == null) {
            Properties props = System.getProperties();
            Properties configSpecifiedOrbProperties = config.getOrbProperties();
            props.putAll(configSpecifiedOrbProperties);
            if (config.getOrbClass() != null) {
                props.put("org.omg.CORBA.ORBClass", config.getOrbClass());
            }
            if (config.getOrbSingletonClass() != null) {
                props.put("org.omg.CORBA.ORBSingletonClass", config.getOrbSingletonClass());
            }
            List<String> orbArgs = config.getOrbArgs();
            defaultORB = ORB.init(orbArgs.toArray(new String[0]), props);
            if (defaultORB == null) {
                LOG.severe("Could not create instance of the ORB");
                throw new CorbaBindingException("Could not create instance of the ORB");
            }
        }
        return defaultORB;
    }

    public static synchronized ORB getAddressSpecificORB(String address,
                                                         Properties props,
                                                         List<String> orbArgs) {
        ORB orb = orbList.get(getORBNameFromAddress(address));
        if (orb == null) {
            orb = createAddressSpecificORB(address, props, orbArgs);
        }
        return orb;
    }

    private static ORB createAddressSpecificORB(String address,
                                                Properties props,
                                                List<String> orbArgs) {
        final URI addressURI;
        try {
            addressURI = new URI(address);
        } catch (URISyntaxException ex) {
            throw new CorbaBindingException("Unable to create ORB with address " + address);
        }

        String scheme = addressURI.getScheme();
        // A corbaloc address gives us host and port information to use when setting up the
        // endpoint for the ORB.  Other types of references will just create ORBs on the
        // host and port used when no preference has been specified.
        if ("corbaloc".equals(scheme)) {
            String schemeSpecificPart = addressURI.getSchemeSpecificPart();
            int keyIndex = schemeSpecificPart.indexOf('/');
            String corbaAddr = schemeSpecificPart.substring(0, keyIndex);

            int index = corbaAddr.indexOf(':');
            String protocol = "iiop";
            if (index != 0) {
                protocol = corbaAddr.substring(0, index);
            }
            int oldIndex = index;
            index = corbaAddr.indexOf(':', oldIndex + 1);
            String host = corbaAddr.substring(oldIndex + 1, index);
            String port = corbaAddr.substring(index + 1);

            props.put("yoko.orb.oa.endpoint", new String(protocol + " --host " + host + " --port " + port));
            // WHAT to do for non-yoko orb?
        } else if ("corbaname".equals(scheme)) {
            String schemeSpecificPart = addressURI.getSchemeSpecificPart();
            if (schemeSpecificPart.startsWith(":")) {
                schemeSpecificPart = schemeSpecificPart.substring(1);
            }
            int idx = schemeSpecificPart.indexOf(':');

            props.put("org.omg.CORBA.ORBInitialHost", schemeSpecificPart.substring(0, idx));
            props.put("org.omg.CORBA.ORBInitialPort", schemeSpecificPart.substring(idx + 1));
        } else if ("file".equals(scheme)
            || "relfile".equals(scheme)
            || "IOR".equals(scheme)
            || "ior".equals(scheme)) {
            //use defaults
        } else {
            throw new CorbaBindingException("Unsupported address scheme type " + scheme);
        }
        ORB orb = ORB.init(orbArgs.toArray(new String[0]), props);
        orbList.put(getORBNameFromAddress(address), orb);

        return orb;
    }



    private static String getORBNameFromAddress(String address) {
        String name;

        final URI addressURI;
        try {
            addressURI = new URI(address);
        } catch (URISyntaxException ex) {
            throw new CorbaBindingException("Unable to locate ORB with address " + address);
        }

        String scheme = addressURI.getScheme();
        if ("corbaloc".equals(scheme) || "corbaname".equals(scheme)) {
            String schemeSpecificPart = addressURI.getSchemeSpecificPart();
            if (schemeSpecificPart.startsWith(":")) {
                schemeSpecificPart = schemeSpecificPart.substring(1);
            }
            int keyIndex = schemeSpecificPart.indexOf('/');
            if (keyIndex != -1) {
                name = schemeSpecificPart.substring(0, keyIndex);
            } else {
                name = schemeSpecificPart;
            }
            if (addressURI.getRawQuery() != null) {
                name += addressURI.getRawQuery();
            }
        } else if ("IOR".equals(scheme) || "ior".equals(scheme)) {
            name = addressURI.toString();
        } else if ("file".equals(scheme) || "relfile".equals(scheme)) {
            name = addressURI.getPath();
            if (name == null) {
                name = addressURI.getSchemeSpecificPart();
            }
        } else {
            throw new CorbaBindingException("Unsupported address scheme type " + scheme);
        }

        return name;
    }

    // This indicates that we need to keep the ORB alive.  This allows multiple objects to share the
    // same ORB and not have one of the objects destroy it while other objects are using it.
    public static synchronized void keepORBAlive(String address) {
        Integer count = orbUseCount.get(getORBNameFromAddress(address));

        if (count == null) {
            orbUseCount.put(getORBNameFromAddress(address), 1);
        } else {
            orbUseCount.put(getORBNameFromAddress(address), count + 1);
        }
    }

    // Signals that the ORB should be tested to see if it can be destroyed.  Actual destruction will
    // only occur if the ORB is not being used by someone else.  If it is, then we simply decrement
    // the count.
    public static synchronized void destroyORB(String address, ORB orb) throws CorbaBindingException {
        Integer count = orbUseCount.get(getORBNameFromAddress(address));

        if (count == null) {
            return;
        }

        count = count - 1;

        if (count < 1) {
            // We shouldn't have anyone waiting on this ORB.  Destroy it.
            orbUseCount.remove(getORBNameFromAddress(address));
            orbList.remove(getORBNameFromAddress(address));
            try {
                orb.destroy();
            } catch (Exception ex) {
                throw new CorbaBindingException(ex);
            }
        } else {
            orbUseCount.put(getORBNameFromAddress(address), count);
        }
    }

}
