/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.cmp.jdbc2;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.RemoveException;
import javax.sql.DataSource;
import org.jboss.as.cmp.CmpConfig;
import org.jboss.as.cmp.bridge.EntityBridgeInvocationHandler;
import org.jboss.as.cmp.bridge.FieldBridge;
import org.jboss.as.cmp.component.CmpEntityBeanComponent;
import org.jboss.as.cmp.context.CmpEntityBeanContext;
import org.jboss.as.cmp.ejbql.Catalog;
import org.jboss.as.cmp.jdbc.JDBCEntityPersistenceStore;
import org.jboss.as.cmp.jdbc.JDBCQueryCommand;
import org.jboss.as.cmp.jdbc.JDBCStartCommand;
import org.jboss.as.cmp.jdbc.JDBCStopCommand;
import static org.jboss.as.cmp.jdbc.JDBCStoreManager.CREATE_TABLES;
import static org.jboss.as.cmp.jdbc.JDBCStoreManager.EXISTING_TABLES;
import org.jboss.as.cmp.jdbc.JDBCTypeFactory;
import org.jboss.as.cmp.jdbc.bridge.JDBCAbstractEntityBridge;
import org.jboss.as.cmp.jdbc.metadata.JDBCEntityCommandMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCEntityMetaData;
import org.jboss.as.cmp.jdbc.metadata.JDBCQueryMetaData;
import org.jboss.as.cmp.jdbc2.bridge.EJBSelectBridge;
import org.jboss.as.cmp.jdbc2.bridge.JDBCEntityBridge2;
import org.jboss.as.cmp.jdbc2.schema.EntityTable;
import org.jboss.as.cmp.jdbc2.schema.Schema;
import org.jboss.as.cmp.keygenerator.KeyGeneratorFactory;
import org.jboss.as.cmp.keygenerator.KeyGeneratorFactoryRegistry;
import org.jboss.as.server.deployment.AttachmentKey;
import org.jboss.as.server.deployment.AttachmentList;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.logging.Logger;
import org.jboss.msc.inject.Injector;
import org.jboss.msc.value.InjectedValue;
import org.jboss.tm.TransactionLocal;


/**
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version <tt>$Revision: 94413 $</tt>
 */
public class JDBCStoreManager2 implements JDBCEntityPersistenceStore {

    private final DeploymentUnit deploymentUnit;
    private CmpEntityBeanComponent component;
    private final JDBCEntityMetaData metaData;
    private final CmpConfig cmpConfig;
    private Logger log;
    private JDBCEntityBridge2 entityBridge;
    private EntityBridgeInvocationHandler bridgeInvocationHandler;
    private JDBCTypeFactory typeFactory;
    private Schema schema;
    private Catalog catalog;

    private final InjectedValue<KeyGeneratorFactoryRegistry> keyGeneratorFactoryRegistry = new InjectedValue<KeyGeneratorFactoryRegistry>();

    private QueryFactory queryFactory;
    private CreateCommand createCmd;
    private JDBCStartCommand startCmd;
    private JDBCStopCommand stop;

    private final TransactionLocal cascadeDeleteRegistry = new TransactionLocal() {
        protected Object initialValue() {
            return new HashMap();
        }
    };

    public JDBCStoreManager2(final DeploymentUnit deploymentUnit, final JDBCEntityMetaData metaData, final CmpConfig cmpConfig, final Catalog catalog) {
        this.deploymentUnit = deploymentUnit;
        this.metaData = metaData;
        this.catalog = catalog;
        this.cmpConfig = cmpConfig;
    }

    // Public
    public static final AttachmentKey<Schema> SCHEMA = AttachmentKey.create(Schema.class);

    public Schema getSchema() {
        schema = deploymentUnit.getAttachment(SCHEMA);
        if (schema == null) {
            schema = new Schema(component.getComponentName());
            deploymentUnit.putAttachment(SCHEMA, schema);
        }
        return schema;
    }

    public Catalog getCatalog() {
        return catalog;
    }

    public QueryFactory getQueryFactory() {
        return queryFactory;
    }

    public boolean registerCascadeDelete(Object key, Object value) {
        Map map = (Map) cascadeDeleteRegistry.get();
        return map.put(key, value) == null;
    }

    public boolean isCascadeDeleted(Object key) {
        Map map = (Map) cascadeDeleteRegistry.get();
        return map.containsKey(key);
    }

    public void unregisterCascadeDelete(Object key) {
        Map map = (Map) cascadeDeleteRegistry.get();
        map.remove(key);
    }

    // Service implementation
    private static final AttachmentKey<AttachmentList<JDBCStoreManager2>> CREATED_MANAGERS = AttachmentKey.createList(JDBCStoreManager2.class);


    public void init(final CmpEntityBeanComponent component) {
        this.component = component;
        deploymentUnit.addToAttachmentList(CREATED_MANAGERS, this);
        try {
            initStoreManager();
        } catch (Exception e) {
            throw new RuntimeException("Failed to init store manager", e);
        }
        entityBridge.resolveRelationships();
        try {
            startStoreManager();
        } catch (Exception e) {
            throw new RuntimeException("Failed to start store manager", e);
        }
        startCmd.addForeignKeyConstraints();
    }

    public void stop() {
        if (stop != null) {
            List<JDBCStoreManager2> managers = deploymentUnit.getAttachment(CREATED_MANAGERS);
            while (managers != null && !managers.isEmpty()) {
                int stoppedInIteration = 0;
                for (Iterator<JDBCStoreManager2> i = managers.iterator(); i.hasNext(); ) {
                    JDBCStoreManager2 manager = i.next();
                    if (manager.stop.execute()) {
                        i.remove();
                        try {
                            manager.entityBridge.stop();
                        } catch (Exception e) {
                            log.error("Failed to stop entity bridge.", e);
                        }
                        ++stoppedInIteration;
                    }
                }

                if (stoppedInIteration == 0) {
                    break;
                }
            }
        }
    }

    // JDBCEntityPersistenceStore implementation

    public JDBCAbstractEntityBridge getEntityBridge() {
        return entityBridge;
    }

    public JDBCEntityMetaData getMetaData() {
        return metaData;
    }

    public JDBCTypeFactory getJDBCTypeFactory() {
        return typeFactory;
    }

    public CmpEntityBeanComponent getComponent() {
        return component;
    }


    // EntityPersistenceStore implementation

    public Object createBeanClassInstance() throws Exception {
        return null; // TODO: jeb - Create a proxy for the component
    }

    public void initEntity(CmpEntityBeanContext ctx) {
        entityBridge.initPersistenceContext(ctx);
        entityBridge.initInstance(ctx);
    }

    public Object createEntity(Method m, Object[] args, CmpEntityBeanContext ctx)
            throws CreateException {
        /*
        Object pk;
        PersistentContext pctx = (PersistentContext) ctx.getPersistenceContext();
        if(ctx.getId() == null)
        {
           pk = entityBridge.extractPrimaryKeyFromInstance(ctx);

           if(pk == null)
           {
              throw new CreateException("Primary key for created instance is null.");
           }

           pctx.setPk(pk);
        }
        else
        {
           // insert-after-ejb-post-create
           try
           {
              pctx.flush();
           }
           catch(SQLException e)
           {
              if("23000".equals(e.getSQLState()))
              {
                 throw new DuplicateKeyException("Unique key violation or invalid foreign key value: pk=" + ctx.getId());
              }
              else
              {
                 throw new CreateException("Failed to create instance: pk=" + ctx.getId() +
                    ", state=" + e.getSQLState() +
                    ", msg=" + e.getMessage());
              }
           }
           pk = ctx.getId();
        }
        return pk;
        */
        return createCmd.execute(m, args, ctx);
    }

    public Object postCreateEntity(Method m, Object[] args, CmpEntityBeanContext ctx) throws CreateException {
        return null;
    }

    public Object findEntity(Method finderMethod,
                             Object[] args,
                             CmpEntityBeanContext instance, final JDBCQueryCommand.EntityProxyFactory factory)
            throws FinderException {
        QueryCommand query = queryFactory.getQueryCommand(finderMethod);
        return query.fetchOne(schema, args, factory);
    }

    public Collection findEntities(Method finderMethod,
                                   Object[] args,
                                   CmpEntityBeanContext instance, final JDBCQueryCommand.EntityProxyFactory factory)
            throws FinderException {
        QueryCommand query = queryFactory.getQueryCommand(finderMethod);
        return query.fetchCollection(schema, args, factory);
    }

    public void activateEntity(CmpEntityBeanContext ctx) {
        entityBridge.initPersistenceContext(ctx);
    }

    public void loadEntity(CmpEntityBeanContext ctx) {
        try {
            EntityTable.Row row = entityBridge.getTable().loadRow(ctx.getPrimaryKey());
            PersistentContext pctx = new PersistentContext(entityBridge, row);
            ctx.setPersistenceContext(pctx);
        } catch (EJBException e) {
            log.error("Failed to load instance of " + entityBridge.getEntityName() + " with pk=" + ctx.getPrimaryKey(), e);
            throw e;
        } catch (Exception e) {
            throw new EJBException(
                    "Failed to load instance of " + entityBridge.getEntityName() + " with pk=" + ctx.getPrimaryKey(), e
            );
        }
    }

    public boolean isStoreRequired(CmpEntityBeanContext instance) {
        return entityBridge.isStoreRequired(instance);
    }

    public boolean isModified(CmpEntityBeanContext instance) throws Exception {
        return entityBridge.isModified(instance);
    }

    public void storeEntity(CmpEntityBeanContext instance) {
        // scary?
    }

    public void passivateEntity(CmpEntityBeanContext ctx) {
        JDBCEntityBridge2.destroyPersistenceContext(ctx);
    }

    public void removeEntity(CmpEntityBeanContext ctx) throws RemoveException {
        entityBridge.remove(ctx);
        PersistentContext pctx = (PersistentContext) ctx.getPersistenceContext();
        pctx.remove();
    }

    // Private

    protected void initStoreManager() {
        if (log.isDebugEnabled()) {
            log.debug("Initializing CMP plugin for " + component.getComponentName());
        }

        // setup the type factory, which is used to map java types to sql types.
        typeFactory = new JDBCTypeFactory(metaData.getTypeMapping(),
                metaData.getJDBCApplication().getValueClasses(),
                metaData.getJDBCApplication().getUserTypeMappings()
        );

        entityBridge = new JDBCEntityBridge2(this, metaData);
        entityBridge.init();

        Catalog catalog = getCatalog();
        catalog.addEntity(entityBridge);

        stop = new JDBCStopCommand(this);
    }

    protected void startStoreManager() throws Exception {
        queryFactory = new QueryFactory(entityBridge);
        queryFactory.init();

        bridgeInvocationHandler = new EntityBridgeInvocationHandler(createFieldMap(entityBridge), createSelectorMap(entityBridge, queryFactory));

        startCmd = new JDBCStartCommand(this);
        startCmd.execute();

        final JDBCEntityCommandMetaData entityCommand = getMetaData().getEntityCommand();
        if (entityCommand == null || "default".equals(entityCommand.getCommandName())) {
            createCmd = new ApplicationPkCreateCommand();
        } else {
            final Class cmdClass = entityCommand.getCommandClass();
            if (cmdClass == null) {
                throw new RuntimeException(
                        "entity-command class name is not specified for entity " + entityBridge.getEntityName()
                );
            }

            try {
                createCmd = (CreateCommand) cmdClass.newInstance();
            } catch (ClassCastException cce) {
                throw new RuntimeException("Entity command " + cmdClass + " does not implement " + CreateCommand.class);
            }
        }

        createCmd.init(this);
    }

    public CmpConfig getCmpConfig() {
        return cmpConfig;
    }

    public DataSource getDataSource(final String name) {
        return null; // TODO: jeb - inject these as needed
    }

    public boolean hasCreateTable(String entityName) {
        final List<String> tables = deploymentUnit.getAttachment(CREATE_TABLES);
        return tables != null && tables.contains(entityName);
    }

    public void addCreateTable(String entityName) {
        deploymentUnit.addToAttachmentList(CREATE_TABLES, entityName);
    }

    public void addExistingTable(String entityName) {
        deploymentUnit.addToAttachmentList(EXISTING_TABLES, entityName);
    }

    public EntityBridgeInvocationHandler getInvocationHandler() {
        return bridgeInvocationHandler;
    }

    private static Map<String, Method> getAbstractAccessors(Class<?> beanClass) {
        Method[] methods = beanClass.getMethods();
        Map<String, Method> abstractAccessors = new HashMap<String, Method>(methods.length);

        for (Method method : methods) {
            if (Modifier.isAbstract(method.getModifiers())) {
                String methodName = method.getName();
                if (methodName.startsWith("get") || methodName.startsWith("set")) {
                    abstractAccessors.put(methodName, method);
                }
            }
        }
        return abstractAccessors;
    }

    private static Map<String, EntityBridgeInvocationHandler.BridgeInvoker> createFieldMap(JDBCEntityBridge2 entityBridge) {
        Map<String, Method> abstractAccessors = getAbstractAccessors(entityBridge.getMetaData().getEntityClass());

        List<FieldBridge> fields = entityBridge.getFields();
        Map<String, EntityBridgeInvocationHandler.BridgeInvoker> map = new HashMap<String, EntityBridgeInvocationHandler.BridgeInvoker>(fields.size() * 2);
        for (FieldBridge field : fields) {

            // get the names
            String fieldName = field.getFieldName();
            String fieldBaseName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
            String getterName = "get" + fieldBaseName;
            String setterName = "set" + fieldBaseName;

            // get the accessor methods
            Method getterMethod = (Method) abstractAccessors.get(getterName);
            Method setterMethod = (Method) abstractAccessors.get(setterName);

            // getters and setters must come in pairs
            if (getterMethod != null && setterMethod == null) {
                throw new RuntimeException("Getter was found but, no setter was found for field: " + fieldName);
            } else if (getterMethod == null && setterMethod != null) {
                throw new RuntimeException("Setter was found but, no getter was found for field: " + fieldName);
            } else if (getterMethod != null && setterMethod != null) {
                // add methods
                map.put(getterMethod.getName(), new EntityBridgeInvocationHandler.FieldGetInvoker(field));
                map.put(setterMethod.getName(), new EntityBridgeInvocationHandler.FieldSetInvoker(field));

                // remove the accessors (they have been used)
                abstractAccessors.remove(getterName);
                abstractAccessors.remove(setterName);
            }
        }
        return Collections.unmodifiableMap(map);
    }

    private static Map<Method, EntityBridgeInvocationHandler.BridgeInvoker> createSelectorMap(JDBCEntityBridge2 entityBridge, QueryFactory queryFactory) {
        Collection<JDBCQueryMetaData> queries = entityBridge.getMetaData().getQueries();
        Map<Method, EntityBridgeInvocationHandler.BridgeInvoker> selectorsByMethod = new HashMap<Method, EntityBridgeInvocationHandler.BridgeInvoker>(queries.size());
        for (JDBCQueryMetaData metadata : queries) {
            if (metadata.getMethod().getName().startsWith("ejbSelect")) {
                try {
                    QueryCommand queryCommand = queryFactory.getQueryCommand(metadata.getMethod());
                    Schema schema = ((JDBCStoreManager2) entityBridge.getManager()).getSchema();
                    EJBSelectBridge ejbSelectBridge = new EJBSelectBridge((JDBCStoreManager2) entityBridge.getManager(), entityBridge.getComponent(), schema, metadata, queryCommand);
                    selectorsByMethod.put(metadata.getMethod(), ejbSelectBridge);
                } catch (FinderException e) {
                    throw new RuntimeException(e.getMessage());
                }
            }
        }

        return selectorsByMethod;
    }

    public KeyGeneratorFactory getKeyGeneratorFactory(final String name) {
        return keyGeneratorFactoryRegistry.getValue().getFactory(name);
    }

    public Injector<KeyGeneratorFactoryRegistry> getKeyGeneratorFactoryInjector() {
        return keyGeneratorFactoryRegistry;
    }
}
