/*
 * Copyright (C) 2012  Bull S. A. S.
 * Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation
 * version 2.1 of the License.
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA  02110-1301, USA.
 */

package org.ow2.jonas.datasource.internal;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.XADataSource;

import org.apache.felix.ipojo.annotations.Bind;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Instantiate;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.ow2.easybeans.api.EZBServer;
import org.ow2.jonas.Version;
import org.ow2.jonas.datasource.DataSourceService;
import org.ow2.jonas.dbm.internal.DBMConstants;
import org.ow2.jonas.generators.raconfig.RAConfig;
import org.ow2.jonas.generators.raconfig.RAConfigException;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.loader.OSGiClassLoader;
import org.ow2.jonas.lib.service.AbsServiceImpl;
import org.ow2.jonas.lib.util.JonasObjectName;
import org.ow2.jonas.properties.ServerProperties;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.service.ServiceException;
import org.ow2.util.archive.api.IArchive;
import org.ow2.util.archive.impl.ArchiveManager;
import org.ow2.util.ee.deploy.api.deployable.IDeployable;
import org.ow2.util.ee.deploy.api.deployer.DeployerException;
import org.ow2.util.ee.deploy.api.deployer.IDeployerManager;
import org.ow2.util.ee.deploy.api.deployer.UnsupportedDeployerException;
import org.ow2.util.ee.deploy.api.helper.DeployableHelperException;
import org.ow2.util.ee.deploy.impl.helper.DeployableHelper;
import org.ow2.util.event.api.IEventListener;
import org.ow2.util.event.api.IEventService;
import org.ow2.util.execution.ExecutionResult;
import org.ow2.util.execution.IExecution;
import org.ow2.util.execution.helper.RunnableHelper;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.ow2.util.plan.bindings.deploymentplan.maven2.Maven2Deployment;
import org.ow2.util.plan.bindings.exceptions.InvalidDeploymentException;
import org.ow2.util.plan.fetcher.api.IResourceFetcher;
import org.ow2.util.plan.fetcher.api.IResourceFetcherFactoryManager;
import org.ow2.util.plan.fetcher.api.exceptions.FetcherException;
import org.ow2.util.plan.fetcher.api.exceptions.FetcherFactoryException;
import org.ow2.util.plan.fetcher.api.exceptions.ResourceFetcherNotResolvedException;
import org.ow2.util.plan.fetcher.api.exceptions.ResourceNotFoundException;
import org.ow2.util.plan.repository.api.IRepositoryManager;

/**
 * @author Loic Albertin
 */
@Component(immediate = false, architecture = true, propagation = true)
@Provides(specifications = {DataSourceService.class})
public class DataSourceServiceImpl extends AbsServiceImpl implements DataSourceService {

    /**
     * Logger.
     */
    private Log logger = LogFactory.getLog(DataSourceServiceImpl.class);

    /**
     * Resource fetcher factory
     */
    @Requires
    protected IResourceFetcherFactoryManager resourceFetcherFactoryManager;

    /**
     * The default RAR creation path.
     */
    private static final String DEFAULT_ARCHIVE_PATH = "datasources-service" + File.separator + "datasources";

    /**
     * Repository manager
     */
    @Requires
    protected IRepositoryManager repositoryManager;

    /**
     * The Deployer Manager that will be used to deploy RAR files.
     */
    @Requires
    private IDeployerManager deployerManager = null;

    /**
     * Registry Service.
     */
    @Requires
    private RegistryService registryService;

    private InitialContext initialContext;

    /**
     * Working directory
     */
    protected File archiveFile = null;


    /**
     * The EventService.
     */
    @Requires
    private IEventService eventService = null;


    private IEventListener eventListener = null;

    private List<String> deployedDSJNDINames = new ArrayList<String>();

    /**
     * The JMX Service
     */
    @Requires
    private JmxService jmxService;

    /**
     * Server Properties.
     */
    @Requires
    protected ServerProperties serverrarProperties;

    private String jdbcDMRarPath = null;
    private String jdbcDSRarPath = null;
    private String jdbcXARarPath = null;

    private String resolvePath(final Maven2Deployment deployment) {
        try {
            IResourceFetcher fetcher = this.resourceFetcherFactoryManager.getResourceFetcher(deployment);
            fetcher.setRepositoryManager(repositoryManager);
            fetcher.setDeployment(deployment);
            fetcher.resolve();
            return fetcher.getResource().getPath();
        } catch (FetcherException e) {
            e.printStackTrace();
        } catch (ResourceNotFoundException e) {
            e.printStackTrace();
        } catch (InvalidDeploymentException e) {
            e.printStackTrace();
        } catch (FetcherFactoryException e) {
            e.printStackTrace();
        } catch (ResourceFetcherNotResolvedException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void resolveJDBCRars() {
        Maven2Deployment deployment;
        if (jdbcDMRarPath == null) {
            deployment = new Maven2Deployment();
            deployment.setGroupId("org.ow2.jonas");
            deployment.setArtifactId("jonas-jca-jdbc-dm");
            deployment.setVersion(Version.getNumber());
            deployment.setType("rar");

            jdbcDMRarPath = resolvePath(deployment);
        }

        if (jdbcDSRarPath == null) {
            deployment = new Maven2Deployment();
            deployment.setGroupId("org.ow2.jonas");
            deployment.setArtifactId("jonas-jca-jdbc-ds");
            deployment.setVersion(Version.getNumber());
            deployment.setType("rar");

            jdbcDSRarPath = resolvePath(deployment);
        }

        if (jdbcXARarPath == null) {
            deployment = new Maven2Deployment();
            deployment.setGroupId("org.ow2.jonas");
            deployment.setArtifactId("jonas-jca-jdbc-xa");
            deployment.setVersion(Version.getNumber());
            deployment.setType("rar");

            jdbcXARarPath = resolvePath(deployment);
        }

    }

    public String deployDataSource(final String className, final String description,
            final String url, final String user, final String password, final String databaseName, final int portNumber,
            final String serverName, final int isolationLevel, final boolean transactional, final int initialPoolSize,
            final int maxPoolSize, final int minPoolSize, final int maxIdleTime, final int maxStatements,
            final int loginTimeout, final Map<String, String> properties) {

        //Generate rarProperties
        final Properties rarProperties = generateRarProperties(className, description, url,
                user, password, databaseName, portNumber,
                serverName, isolationLevel, transactional, initialPoolSize,
                maxPoolSize, minPoolSize, maxIdleTime, maxStatements,
                loginTimeout, properties);

        final String jndiDataSourceName = generateDataSourceName(rarProperties);


        try {
            if (initialContext.lookup(jndiDataSourceName) != null) {
                //A corresponding data source already exists.
                return jndiDataSourceName;
            }
        } catch (NamingException e) {
            logger.debug("Unable to lookup {0} into initial context trying to deploy a RAR for this datasource",
                    jndiDataSourceName);
        }


        rarProperties.setProperty(DBMConstants.NAME, jndiDataSourceName);
        String fileOut = null;
        String fileName = "";
        try {
            fileName = "ds-" + jndiDataSourceName.replace('/', '_') + "-";
            fileName = fileName.replace(":", "-");
            fileOut = File.createTempFile(fileName, ".rar", archiveFile).getAbsolutePath();
        } catch (IOException e1) {
            String msg =
                    "Error while creating temp file <" + fileName + ".rar> in directory <" + archiveFile.getPath() + ">";
            throw new ServiceException(msg, e1);
        }

        this.resolveJDBCRars();
        try {
            String fileIn = jdbcDMRarPath;
            String rarLinkExtension = "DM";

            IExecution<Class<?>> loadClassExecution = new IExecution<Class<?>>() {
                public Class<?> execute() throws Exception {
                    OSGiClassLoader osgiClassLoader = new OSGiClassLoader();
                    return osgiClassLoader.loadClass(className);
                }
            };
            ExecutionResult<Class<?>> executionResult =
                    new RunnableHelper<Class<?>>()
                            .execute(OSGiClassLoader.class.getClassLoader(), loadClassExecution);
            if (executionResult.hasException()) {
                logger.fatal("Can't load JDBC driver", executionResult.getException());
                throw new ServiceException("Can't load JDBC driver", executionResult.getException());
            }
            final Class<?> clazz = executionResult.getResult();
            if (DataSource.class.isAssignableFrom(clazz)) {
                fileIn = jdbcDSRarPath;
                rarLinkExtension = "DS";
            } else if (XADataSource.class.isAssignableFrom(clazz)) {
                fileIn = jdbcXARarPath;
                rarLinkExtension = "XA";
            } else if (ConnectionPoolDataSource.class.isAssignableFrom(clazz)) {
                fileIn = jdbcXARarPath;
                rarLinkExtension = "XA";
            }

            logger.info("Generate RAR {0} -> {1}", fileIn, fileOut);


            RAConfig.generateRars(rarProperties, "JOnASJDBC_" + rarLinkExtension, fileIn, fileOut);
        } catch (RAConfigException e) {
            logger.error("Failure to deploy datasource {0}", jndiDataSourceName);
            throw new ServiceException("Failure to deploy datasource " + jndiDataSourceName, e);
        }

        /*
        * Deploy .rar 1- Create IArchive 2- Create Deployable 3- Deploy
        */

        // Create an IArchive
        File generatedFile = new File(fileOut);

        if (!generatedFile.exists()) {
            logger.error("RAR was not generated with RAConfig- {0}", fileOut);
            throw new ServiceException("RAR was not generated with RAConfig- {0} " + fileOut);
        }

        IArchive archiveRar = ArchiveManager.getInstance().getArchive(generatedFile);
        if (archiveRar == null) {
            throw new ServiceException("Null archive for file " + fileOut);
        }

        // Create a Deployable
        IDeployable<?> rarDeployable = null;
        try {
            rarDeployable = DeployableHelper.getDeployable(archiveRar);
        } catch (DeployableHelperException e) {
            throw new ServiceException("Unable to deploy generated RAR ", e);
        }
        if (rarDeployable == null) {
            throw new ServiceException("Null deployable for archive " + archiveRar);
        }

        // Deploy
        try {
            this.deployerManager.deploy(rarDeployable);
        } catch (DeployerException e) {
            throw new ServiceException("Exception while deploying deployable " + rarDeployable, e);
        } catch (UnsupportedDeployerException e) {
            throw new ServiceException("No deployer found for deployable " + rarDeployable, e);
        }

        // Here deployment succeeded.
        deployedDSJNDINames.add(jndiDataSourceName);
        return jndiDataSourceName;
    }

    private String generateDataSourceName(Properties rarProperties) {
        //TODO implement a smartest and configurable caching algorithm
        final StringBuilder sb = new StringBuilder("DS_");
        sb.append(rarProperties.getProperty(DBMConstants.CLASSNAME, ""));
        sb.append(rarProperties.getProperty("datasource.databaseName", ""));
        sb.append(rarProperties.getProperty("datasource.serverName", ""));
        sb.append(rarProperties.getProperty("datasource.portNumber", ""));
        sb.append(rarProperties.getProperty(DBMConstants.USERNAME, ""));
        sb.append(rarProperties.getProperty(DBMConstants.PASSWORD, ""));
        sb.append(rarProperties.getProperty(DBMConstants.ISOLATIONLEVEL, ""));
        sb.append(rarProperties.getProperty(DBMConstants.INITCONPOOL, ""));
        sb.append(rarProperties.getProperty(DBMConstants.MINCONPOOL, ""));
        sb.append(rarProperties.getProperty(DBMConstants.MAXCONPOOL, ""));
        sb.append(rarProperties.getProperty(DBMConstants.PSTMTMAX, ""));
        sb.append(rarProperties.getProperty(DBMConstants.MAXOPENTIME, ""));
        sb.append(rarProperties.getProperty("datasource.loginTimeout", ""));
        return sb.toString();
    }

    private Properties generateRarProperties(String className, String description, String url, String user,
            String password, String databaseName, int portNumber, String serverName, int isolationLevel,
            boolean transactional, int initialPoolSize, int maxPoolSize, int minPoolSize, int maxIdleTime,
            int maxStatements, int loginTimeout, Map<String, String> properties) {
        final Properties rarProperties = new Properties();
        //Use URL parameter only if no other more specific parameters exists
        if (url != null && !url.equals("")) {
            rarProperties.setProperty(DBMConstants.URL, url);

            //Extract parameters from url
            // pattern items in parenthesis are optionals: ...://(user(:password)@)serverName(:portNumber)/databaseName

            final Matcher matcher = Pattern.compile(".+://([^/]+)/(.+)$").matcher(url);
            if (matcher.matches() && matcher.groupCount() == 2) {
                String dbConnection = matcher.group(1);
                rarProperties.setProperty("datasource.databaseName", matcher.group(2));
                final int atIndex = dbConnection.indexOf("@");
                if (atIndex != -1) {
                    dbConnection = dbConnection.substring(atIndex + 1);
                    if (atIndex > 0) {
                        String userAndPass = dbConnection.substring(0, atIndex - 1);
                        final int colonIndex = userAndPass.indexOf(":");
                        if (colonIndex != -1) {
                            rarProperties.setProperty(DBMConstants.PASSWORD, userAndPass.substring(colonIndex + 1));
                            if (colonIndex > 0) {
                                userAndPass = userAndPass.substring(0, colonIndex - 1);
                            }
                        }
                        rarProperties.setProperty(DBMConstants.USERNAME, userAndPass);
                    }
                }
                final int colonIndex = dbConnection.indexOf(":");
                if (colonIndex != -1) {
                    rarProperties.setProperty("datasource.portNumber", dbConnection.substring(colonIndex + 1));
                    if (colonIndex > 0) {
                        dbConnection = dbConnection.substring(0, colonIndex - 1);
                    }
                }
                rarProperties.setProperty("datasource.serverName", dbConnection);
            }
        }
        if (user != null && !user.equals("")) {
            rarProperties.setProperty(DBMConstants.USERNAME, user);
        }
        if (password != null && !password.equals("")) {
            rarProperties.setProperty(DBMConstants.PASSWORD, password);
        }
        if (databaseName != null && !databaseName.equals("")) {
            rarProperties.setProperty("datasource.databaseName", databaseName);
        }
        if (portNumber != -1) {
            rarProperties.setProperty("datasource.portNumber", String.valueOf(portNumber));
        }
        if (serverName != null && !serverName.equals("")) {
            rarProperties.setProperty("datasource.serverName", serverName);
        }

        rarProperties.setProperty(DBMConstants.CLASSNAME, className);

        if (description != null) {
            rarProperties.setProperty(DBMConstants.DESCRIPTION, description);
        }
        if (isolationLevel == Connection.TRANSACTION_READ_COMMITTED) {
            rarProperties.setProperty(DBMConstants.ISOLATIONLEVEL, "read_committed");
        } else if (isolationLevel == Connection.TRANSACTION_READ_UNCOMMITTED) {
            rarProperties.setProperty(DBMConstants.ISOLATIONLEVEL, "read_uncommitted");
        } else if (isolationLevel == Connection.TRANSACTION_SERIALIZABLE) {
            rarProperties.setProperty(DBMConstants.ISOLATIONLEVEL, "serializable");
        } else if (isolationLevel == Connection.TRANSACTION_REPEATABLE_READ) {
            rarProperties.setProperty(DBMConstants.ISOLATIONLEVEL, "repeatable_read");
        } else {
            rarProperties.setProperty(DBMConstants.ISOLATIONLEVEL, "none");
        }

        rarProperties.setProperty(DBMConstants.INITCONPOOL, String.valueOf(initialPoolSize));
        rarProperties.setProperty(DBMConstants.MINCONPOOL, String.valueOf(minPoolSize));
        rarProperties.setProperty(DBMConstants.MAXCONPOOL, String.valueOf(maxPoolSize));
        rarProperties.setProperty(DBMConstants.PSTMTMAX, String.valueOf(maxStatements));
        rarProperties.setProperty(DBMConstants.MAXOPENTIME, String.valueOf(maxIdleTime));

        rarProperties.setProperty("datasource.loginTimeout", String.valueOf(loginTimeout));

        //Add additional properties
        if (properties != null) {
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                rarProperties.setProperty(entry.getKey(), entry.getValue());
            }
        }

        return rarProperties;
    }

    @Override
    protected void doStart() throws ServiceException {
        logger.debug("DataSource Service starting");

        // register mbeans-descriptors
        jmxService.loadDescriptors(getClass().getPackage().getName(), getClass().getClassLoader());
         // Register MBean
        try {
            jmxService.registerModelMBean(this, JonasObjectName.dataSourceService(getDomainName()));
        } catch (Exception e) {
            logger.warn("Cannot register MBean for DataSource service", e);
        }
        /**
         * Create working directory for the deployer.
         */
        if (archiveFile == null) {
            archiveFile = new File(
                    serverrarProperties.getWorkDirectory() + File.separator + DEFAULT_ARCHIVE_PATH + File.separator +
                            serverrarProperties.getServerName());
            archiveFile.mkdirs();
        }

        // Get the InitialContext in an execution block
        IExecution<InitialContext>
                ictxGetter = new IExecution<InitialContext>() {
            public InitialContext execute() throws Exception {
                return registryService.getRegistryContext();
            }
        };

        // Execute
        ExecutionResult<InitialContext> ictxResult =
                new RunnableHelper<InitialContext>().execute(getClass().getClassLoader(), ictxGetter);
        // Throw an ServiceException if needed
        if (ictxResult.hasException()) {
            logger.error("Cannot create initial context when Resource service initializing");
            throw new ServiceException(
                    "Cannot create initial context when Resource service initializing",
                    ictxResult.getException());
        }
        // Store the ref
        initialContext = ictxResult.getResult();

        if (eventService != null) {
            eventListener = new DSDefinitionExtensionListener(this);
            eventService.registerListener(eventListener, EZBServer.NAMING_EXTENSION_POINT);
        }
    }

    @Override
    protected void doStop() throws ServiceException {
        // Unregister MBean
        try {
            jmxService.unregisterModelMBean(JonasObjectName.dataSourceService(getDomainName()));
        } catch (Exception e) {
            logger.debug("Cannot unregister MBean for DataSource service", e);
        }
        if (eventService != null && eventListener != null) {
            eventService.unregisterListener(eventListener);
        }

        for (String deployedDSJNDIName : deployedDSJNDINames) {
            try {
                initialContext.unbind(deployedDSJNDIName);
            } catch (NamingException e) {
                logger.error("Unable to unbind data source " + deployedDSJNDIName + " from JNDI global context", e);
            }
        }
        deployedDSJNDINames.clear();
    }


    //Override parent methods to add iPOJO annotations

    @Validate
    @Override
    public void start() throws ServiceException {
        super.start();
        logger.debug("DataSource Service started");
    }

    @Invalidate
    @Override
    public void stop() throws ServiceException {
        super.stop();
        logger.debug("DataSource Service stopped");
    }

    @Bind(id="ServerProps", optional = false, specification = "org.ow2.jonas.properties.ServerProperties")
    @Override
    public void setServerProperties(ServerProperties props) {
        super.setServerProperties(props);
    }
}
