/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.jdbc.delegate;

import java.sql.Connection;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.jcr.LoginException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.modeshape.jcr.api.Repositories;
import org.modeshape.jdbc.JdbcLocalI18n;
import org.modeshape.jdbc.LocalJcrDriver;
import org.modeshape.jdbc.LocalJcrDriver.JcrContextFactory;

/**
 * The LocalRepositoryDelegate provides a local Repository implementation to access the Jcr layer via JNDI Lookup.
 */
public class LocalRepositoryDelegate extends AbstractRepositoryDelegate {
    public static final RepositoryDelegateFactory FACTORY = new RepositoryDelegateFactory() {};

    private static final String JNDI_EXAMPLE_URL = LocalJcrDriver.JNDI_URL_PREFIX + "{jndiName}";

    protected static final Set<LocalSession> TRANSACTION_IDS = java.util.Collections.synchronizedSet(new HashSet<LocalSession>());

    private JcrContextFactory jcrContext = null;

    public LocalRepositoryDelegate( String url,
                                    Properties info,
                                    JcrContextFactory contextFactory ) {
        super(url, info);

        if (contextFactory == null) {
            jcrContext = new JcrContextFactory() {
                @Override
                public Context createContext( Properties properties ) throws NamingException {
                    return ((properties == null || properties.isEmpty()) ? new InitialContext() : new InitialContext(properties));
                }
            };
        } else {
            this.jcrContext = contextFactory;
        }
    }

    @Override
    protected ConnectionInfo createConnectionInfo( String url,
                                                   Properties info ) {
        return new JNDIConnectionInfo(url, info);
    }

    protected JcrContextFactory getJcrContext() {
        return jcrContext;
    }

    private LocalSession getLocalSession() throws LoginException, NoSuchWorkspaceException, RepositoryException {
        return LocalSession.getLocalSessionInstance().getLocalSession(getRepository(), getConnectionInfo());
    }

    private LocalSession getCurrentLocalSession() {
        return LocalSession.getLocalSessionInstance().getLocalSession();
    }

    @Override
    public String getDescriptor( String descriptorKey ) {
        return getRepository().getDescriptor(descriptorKey);
    }

    @Override
    public NodeType nodeType( String name ) throws RepositoryException {
        LocalSession localSession = getLocalSession();
        return localSession.getSession().getWorkspace().getNodeTypeManager().getNodeType(name);
    }

    @Override
    public List<NodeType> nodeTypes() throws RepositoryException {

        LocalSession localSession = getLocalSession();
        List<NodeType> types = new ArrayList<NodeType>();
        NodeTypeIterator its = localSession.getSession().getWorkspace().getNodeTypeManager().getAllNodeTypes();
        while (its.hasNext()) {
            types.add((NodeType)its.next());
        }
        return types;

    }

    /**
     * This execute method is used for redirection so that the JNDI implementation can control calling execute.
     * 
     * @see java.sql.Statement#execute(java.lang.String)
     */
    @Override
    public QueryResult execute( String query,
                                String language ) throws RepositoryException {
        logger.trace("Executing query: {0}", query);

        // Create the query ...

        final Query jcrQuery = getLocalSession().getSession().getWorkspace().getQueryManager().createQuery(query, language);
        return jcrQuery.execute();
    }

    @Override
    public String explain( String query,
                           String language ) throws RepositoryException {
        logger.trace("Explaining query: {0}", query);

        // Create the query ...

        final org.modeshape.jcr.api.query.Query jcrQuery = (org.modeshape.jcr.api.query.Query)getLocalSession().getSession().getWorkspace().getQueryManager().createQuery(query,
                                                                                                                                                                          language);
        return jcrQuery.explain().getPlan();
    }

    @Override
    protected void retrieveRepository() throws SQLException {
        logger.debug("Creating repository for LocalRepositoryDelegte");

        Repository repository = null;
        Set<String> repositoryNames = null;
        ConnectionInfo connInfo = this.getConnectionInfo();
        assert connInfo != null;

        // Look up the object in JNDI and find the JCR Repository object ...
        String jndiName = connInfo.getRepositoryPath();
        if (jndiName == null) {
            String msg = JdbcLocalI18n.urlMustContainJndiNameOfRepositoryOrRepositoriesObject.text();
            throw new SQLException(msg);
        }

        Context context = null;
        try {
            context = this.jcrContext.createContext(connInfo.getProperties());
        } catch (NamingException e) {
            throw new SQLException(JdbcLocalI18n.unableToGetJndiContext.text(e.getLocalizedMessage()));
        }
        if (context == null) {
            throw new SQLException(JdbcLocalI18n.unableToFindObjectInJndi.text(jndiName));

        }
        String repositoryName = "NotAssigned";
        try {

            Object target = context.lookup(jndiName);
            repositoryName = connInfo.getRepositoryName();

            if (target instanceof Repositories) {
                logger.trace("JNDI Lookup found Repositories ");
                Repositories repositories = (Repositories)target;

                if (repositoryName == null) {
                    repositoryNames = repositories.getRepositoryNames();
                    if (repositoryNames == null || repositoryNames.isEmpty()) {
                        throw new SQLException(JdbcLocalI18n.noRepositoryNamesFound.text());
                    }
                    if (repositoryNames.size() == 1) {
                        repositoryName = repositoryNames.iterator().next();
                        connInfo.setRepositoryName(repositoryName);
                        logger.trace("Setting Repository {0} as default", repositoryName);

                    } else {
                        throw new SQLException(JdbcLocalI18n.objectInJndiIsRepositories.text(jndiName));
                    }
                }
                try {
                    repository = repositories.getRepository(repositoryName);
                } catch (RepositoryException e) {
                    throw new SQLException(JdbcLocalI18n.unableToFindNamedRepository.text(jndiName, repositoryName));
                }
            } else if (target instanceof Repository) {
                logger.trace("JNDI Lookup found a Repository");
                repository = (Repository)target;
                repositoryNames = new HashSet<String>(1);

                if (repositoryName == null) {
                    repositoryName = ("DefaultRepository");
                    connInfo.setRepositoryName(repositoryName);
                }

                repositoryNames.add(repositoryName);
            } else {
                throw new SQLException(JdbcLocalI18n.objectInJndiMustBeRepositoryOrRepositories.text(jndiName));
            }
            assert repository != null;
        } catch (NamingException e) {
            throw new SQLException(JdbcLocalI18n.unableToFindObjectInJndi.text(jndiName), e);
        }
        this.setRepository(repository);
        this.setRepositoryName(repositoryName);
        this.setRepositoryNames(repositoryNames);
    }

    @Override
    public boolean isValid( final int timeout ) throws RepositoryException {

        LocalSession ls = getLocalSession();
        if (!ls.getSession().isLive()) {
            ls.remove();
            return false;
        }

        return true;
    }

    @Override
    public void closeStatement() {
        LocalSession session = getCurrentLocalSession();
        try {
            if (session != null) {
                session.remove();
            }
        } catch (Exception e) {
            // do nothing
        }
    }

    @Override
    public void close() {
        for (LocalSession id : TRANSACTION_IDS) {
            id.remove();
        }
    }

    @SuppressWarnings( "unused" )
    @Override
    public void rollback() throws RepositoryException {
        closeStatement();
    }

    @Override
    public <T> T unwrap( Class<T> iface ) throws SQLException {

        try {
            if (iface.isInstance(this)) {
                return iface.cast(this);
            }

            if (iface.isInstance(Workspace.class)) {
                Workspace workspace = getLocalSession().getSession().getWorkspace();
                return iface.cast(workspace);
            }
        } catch (RepositoryException re) {
            throw new SQLException(re.getLocalizedMessage());
        }

        throw new SQLException(JdbcLocalI18n.classDoesNotImplementInterface.text(Connection.class.getSimpleName(),
                                                                                 iface.getName()));
    }

    class JNDIConnectionInfo extends ConnectionInfo {

        protected JNDIConnectionInfo( String url,
                                      Properties properties ) {
            super(url, properties);
        }

        @Override
        public String getUrlExample() {
            return JNDI_EXAMPLE_URL;
        }

        @Override
        public String getUrlPrefix() {
            return LocalJcrDriver.JNDI_URL_PREFIX;
        }

        @Override
        protected void addRepositoryNamePropertyInfo( List<DriverPropertyInfo> results ) {
            boolean no_errors = results.size() == 0;
            boolean nameRequired = false;
            if (getRepositoryName() == null) {
                boolean found = false;
                if (no_errors) {
                    try {
                        Context context = getJcrContext().createContext(getProperties());
                        Object obj = context.lookup(getRepositoryPath());
                        if (obj instanceof Repositories) {
                            nameRequired = true;
                            found = true;
                        } else if (obj instanceof Repository) {
                            found = true;
                        }
                    } catch (NamingException e) {
                        // do nothing about it ...
                    }
                }
                if (nameRequired || !found) {
                    DriverPropertyInfo info = new DriverPropertyInfo(JdbcLocalI18n.repositoryNamePropertyName.text(), null);
                    info.description = JdbcLocalI18n.repositoryNamePropertyDescription.text();
                    info.required = nameRequired;
                    info.choices = null;
                    results.add(info);
                }
            }
        }
    }

}
