/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * 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.activeio;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.Properties;

import org.activeio.adapter.AsynchToSynchChannelFactoryAdaptor;
import org.activeio.adapter.SynchToAsynchChannelFactoryAdaptor;

import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;

/**
 * A {@see ChannelFactory}uses the requested URI's scheme to determine the
 * actual {@see org.activeio.SynchChannelFactory}or
 * {@see org.activeio.AsynchChannelFactory}implementation to use to create it's
 * {@see org.activeio.Channel}s and {@see org.activeio.ChannelServer}s.
 * 
 * Each URI scheme that {@see ChannelFactory}object handles will have a
 * properties file located at: "META-INF/org.activeio.ChannelFactory/{scheme}".
 * 
 */
public class ChannelFactory implements SynchChannelFactory, AsynchChannelFactory {

    private final HashMap synchChannelFactoryMap = new HashMap();
    private final HashMap asynchChannelFactoryMap = new HashMap();

    
    static public final Executor DEFAULT_EXECUTOR = new PooledExecutor();
    static {
        ((PooledExecutor) DEFAULT_EXECUTOR).setThreadFactory(new ThreadFactory() {
            public Thread newThread(Runnable run) {
                Thread thread = new Thread(run);
                thread.setDaemon(true);
                return thread;
            }
        });
    }

    static private class FactoryFinder {
        private String path;

        public FactoryFinder(String path) {
            this.path = path;
        }

        /**
         * Creates a new instance of the given key
         * 
         * @param key
         *            is the key to add to the path to find a text file
         *            containing the factory name
         * @return a newly created instance
         */
        public Object newInstance(String key) throws IllegalAccessException, InstantiationException, IOException,
                ClassNotFoundException {
            return newInstance(key, null);
        }

        public Object newInstance(String key, String propertyPrefix) throws IllegalAccessException,
                InstantiationException, IOException, ClassNotFoundException {
            return newInstance(doFindFactoryProperies(key), propertyPrefix);
        }

        private Object newInstance(Properties properties, String propertyPrefix) throws ClassNotFoundException,
                InstantiationException, IllegalAccessException, IOException {

            if (propertyPrefix == null)
                propertyPrefix = "";

            String className = properties.getProperty(propertyPrefix + "class");
            if (className == null) {
                throw new IOException("Expected property is missing: " + propertyPrefix + "class");
            }
            Class clazz;
            try {
                clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
            } catch (ClassNotFoundException e) {
                clazz = getClass().getClassLoader().loadClass(className);
            }

            // We could use the properties to pass constructor args or set
            // properties on the object.
            return clazz.newInstance();
        }

        private Properties doFindFactoryProperies(String key) throws IOException, ClassNotFoundException {
            String uri = path + key;

            // lets try the thread context class loader first
            InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(uri);
            if (in == null) {
                in = getClass().getClassLoader().getResourceAsStream(uri);
                if (in == null) {
                    throw new IOException("Could not find factory class for resource: " + uri);
                }
            }

            // lets load the file
            BufferedInputStream reader = null;
            try {
                reader = new BufferedInputStream(in);
                Properties properties = new Properties();
                properties.load(reader);
                return properties;
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                }
            }
        }
    }

    private static FactoryFinder finder = new FactoryFinder("META-INF/org.activeio.ChannelFactory/");

    public SynchChannel openSynchChannel(URI location) throws IOException {
        SynchChannelFactory factory = getSynchChannelFactory(location.getScheme());
        return factory.openSynchChannel(location);
    }

    public SynchChannelServer bindSynchChannel(URI location) throws IOException {
        SynchChannelFactory factory = getSynchChannelFactory(location.getScheme());
        return factory.bindSynchChannel(location);
    }

    public AsynchChannel openAsynchChannel(URI location) throws IOException {
        AsynchChannelFactory factory = getAsynchChannelFactory(location.getScheme());
        return factory.openAsynchChannel(location);
    }

    public AsynchChannelServer bindAsynchChannel(URI location) throws IOException {
        AsynchChannelFactory factory = getAsynchChannelFactory(location.getScheme());
        return factory.bindAsynchChannel(location);
    }

    private SynchChannelFactory getSynchChannelFactory(String protocol) throws IOException {
        try {
            SynchChannelFactory rc = (SynchChannelFactory) synchChannelFactoryMap.get(protocol);
            if (rc == null) {
                try {
                    rc = (SynchChannelFactory) finder.newInstance(protocol, "SynchChannelFactory.");
                } catch (Throwable original) {
                    // try to recovery by using AsynchChannelFactory and adapt
                    // it to be synch.
                    try {
                        AsynchChannelFactory f = (AsynchChannelFactory) finder.newInstance(protocol,
                                "AsynchChannelFactory.");
                        rc = AsynchToSynchChannelFactoryAdaptor.adapt(f);
                    } catch (Throwable e) {
                        // Recovery strategy failed.. throw original exception.
                        throw original;
                    }
                }
                synchChannelFactoryMap.put(protocol, rc);
            }
            return rc;
        } catch (Throwable e) {
            throw (IOException) new IOException("Could not load a SynchChannelFactory for protcol: " + protocol
                    + ", reason: " + e).initCause(e);
        }
    }

    private AsynchChannelFactory getAsynchChannelFactory(String protocol) throws IOException {
        try {
            AsynchChannelFactory rc = (AsynchChannelFactory) asynchChannelFactoryMap.get(protocol);
            if (rc == null) {

                try {
                    rc = (AsynchChannelFactory) finder.newInstance(protocol, "AsynchChannelFactory.");
                } catch (Throwable original) {
                    // try to recovery by using SynchChannelFactory and adapt it
                    // to be asynch.
                    try {
                        SynchChannelFactory f = (SynchChannelFactory) finder.newInstance(protocol,
                                "SynchChannelFactory.");
                        rc = SynchToAsynchChannelFactoryAdaptor.adapt(f);
                    } catch (Throwable e) {
                        // Recovery strategy failed.. throw original exception.
                        throw original;
                    }
                }

                asynchChannelFactoryMap.put(protocol, rc);
            }
            return rc;
        } catch (Throwable e) {
            throw (IOException) new IOException("Could not load a AsynchChannelFactory for protcol: " + protocol
                    + ", reason: " + e).initCause(e);
        }
    }

}