/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.env.jpa;

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceProvider;
import javax.persistence.spi.PersistenceUnitTransactionType;

import com.caucho.amber.manager.AmberPersistenceProvider;
import com.caucho.config.Config;
import com.caucho.config.ConfigException;
import com.caucho.config.LineConfigException;
import com.caucho.config.Names;
import com.caucho.config.inject.BeanBuilder;
import com.caucho.config.inject.CurrentLiteral;
import com.caucho.config.inject.InjectManager;
import com.caucho.config.program.ConfigProgram;
import com.caucho.inject.Module;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.loader.Environment;
import com.caucho.loader.EnvironmentClassLoader;
import com.caucho.loader.EnvironmentEnhancerListener;
import com.caucho.loader.EnvironmentLocal;
import com.caucho.loader.enhancer.ScanClass;
import com.caucho.loader.enhancer.ScanListener;
import com.caucho.util.CharBuffer;
import com.caucho.util.IoUtil;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;

/**
 * Manages the JPA persistence contexts.
 */
@Module
public class PersistenceManager 
  implements ScanListener, EnvironmentEnhancerListener
{
  private static final Logger log
    = Logger.getLogger(PersistenceManager.class.getName());

  private static final L10N L = new L10N(PersistenceManager.class);

  private static final EnvironmentLocal<PersistenceManager> _localManager
    = new EnvironmentLocal<PersistenceManager>();

  private EnvironmentClassLoader _classLoader;
  private ClassLoader _tempLoader;
  
  private HashMap<String, PersistenceUnitManager> _persistenceUnitMap
    = new HashMap<String, PersistenceUnitManager>();
  
  private ArrayList<ConfigProgram> _unitDefaultList
    = new ArrayList<ConfigProgram>();

  private HashMap<String, ArrayList<ConfigProgram>> _unitDefaultMap
    = new HashMap<String, ArrayList<ConfigProgram>>();
  
  private ArrayList<Path> _pendingRootList = new ArrayList<Path>();
  
  private HashMap<String, EntityManager> _persistenceContextMap
    = new HashMap<String, EntityManager>();

  private ArrayList<LazyEntityManagerFactory> _pendingFactoryList
    = new ArrayList<LazyEntityManagerFactory>();

  private PersistenceManager(ClassLoader loader)
  {
    _classLoader = Environment.getEnvironmentClassLoader(loader);
    _localManager.set(this, _classLoader);

    _tempLoader = _classLoader.getNewTempClassLoader();

    _classLoader.addScanListener(this);

    Environment.addEnvironmentListener(this, _classLoader);

    try {
      if (_classLoader instanceof DynamicClassLoader)
        ((DynamicClassLoader) _classLoader).make();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Returns the local container.
   */
  public static PersistenceManager create()
  {
    return create(Thread.currentThread().getContextClassLoader());
  }

  /**
   * Returns the local container.
   */
  public static PersistenceManager create(ClassLoader loader)
  {
    synchronized (_localManager) {
      PersistenceManager container = _localManager.getLevel(loader);

      if (container == null) {
        container = new PersistenceManager(loader);

        _localManager.set(container, loader);
      }

      return container;
    }
  }

  /**
   * Returns the local container.
   */
  public static PersistenceManager getCurrent()
  {
    return getCurrent(Thread.currentThread().getContextClassLoader());
  }

  /**
   * Returns the current environment container.
   */
  public static PersistenceManager getCurrent(ClassLoader loader)
  {
    synchronized (_localManager) {
      return _localManager.get(loader);
    }
  }

  /**
   * Returns the environment's class loader
   */
  public EnvironmentClassLoader getClassLoader()
  {
    return _classLoader;
  }

  /**
   * Returns the JClassLoader.
   */
  public ClassLoader getTempClassLoader()
  {
    return _tempLoader;
  }

  /**
   * Adds a persistence-unit default
   */
  public void addPersistenceUnitDefault(ConfigProgram program)
  {
    _unitDefaultList.add(program);
  }

  /**
   * Returns the persistence-unit default list.
   */
  public ArrayList<ConfigProgram> getPersistenceUnitDefaults()
  {
    return _unitDefaultList;
  }

  void addPersistenceUnit(String name,
                          ConfigJpaPersistenceUnit configJpaPersistenceUnit)
  {
    PersistenceUnitManager pUnit;

    pUnit = createPersistenceUnit(name, PersistenceUnitTransactionType.JTA);

    if (pUnit.getRoot() == null)
      pUnit.setRoot(configJpaPersistenceUnit.getPath());
    
    pUnit.addOverrideProgram(configJpaPersistenceUnit.getProgram());
  }

  /**
   * Adds a persistence-unit default
   */
  public void addPersistenceUnitProxy(String name,
                                      ArrayList<ConfigProgram> program)
  {
    ArrayList<ConfigProgram> oldProgram = _unitDefaultMap.get(name);

    if (oldProgram == null)
      oldProgram = new ArrayList<ConfigProgram>();

    oldProgram.addAll(program);

    _unitDefaultMap.put(name, oldProgram);
  }

  public ArrayList<ConfigProgram> getProxyProgram(String name)
  {
    return _unitDefaultMap.get(name);
  }

  public Class<?> loadTempClass(String name) throws ClassNotFoundException
  {
    return Class.forName(name, false, getTempClassLoader());
  }

  public void init()
  {
  }

  public void start()
  {
    configurePersistenceRoots();
    
    startPersistenceUnits();
  }

  public void configurePersistenceRoots()
  {
    ArrayList<Path> rootList = new ArrayList<Path>();

    synchronized (_pendingRootList) {
      rootList.addAll(_pendingRootList);
      _pendingRootList.clear();
    }

    for (Path root : rootList) {
      parsePersistenceConfig(root);
    }
  }

  /**
   * Adds a persistence root.
   */
  private void parsePersistenceConfig(Path root)
  {
    Path persistenceXml = root.lookup("META-INF/persistence.xml");
    
    if (root.getFullPath().endsWith("WEB-INF/classes/")
        && ! persistenceXml.canRead()) {
      persistenceXml = root.lookup("../persistence.xml");
    }

    if (! persistenceXml.canRead())
      return;

    persistenceXml.setUserPath(persistenceXml.getURL());

    if (log.isLoggable(Level.FINE))
      log.fine(this + " parsing " + persistenceXml.getURL());

    InputStream is = null;

    try {
      is = persistenceXml.openRead();

      ConfigPersistence persistence = new ConfigPersistence(root);

      new Config().configure(persistence, is,
          "com/caucho/amber/cfg/persistence.rnc");

      for (ConfigPersistenceUnit unitConfig : persistence.getUnitList()) {
        PersistenceUnitManager pUnit
          = createPersistenceUnit(unitConfig.getName(),
                                  unitConfig.getTransactionType());

        if (pUnit.getRoot() == null)
          pUnit.setRoot(unitConfig.getRoot());
        
        if (unitConfig.getVersion() != null)
          pUnit.setVersion(unitConfig.getVersion());
        
        pUnit.setPersistenceXmlProgram(unitConfig.getProgram());
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw LineConfigException.create(e);
    } finally {
      try {
        if (is != null)
          is.close();
      } catch (Exception e) {
      }
    }
  }

  private PersistenceUnitManager createPersistenceUnit(String name,
                                                       PersistenceUnitTransactionType transactionType)
  {
    PersistenceUnitManager unit;
    
    synchronized (_persistenceUnitMap) {
      unit = _persistenceUnitMap.get(name);
      
      if (unit != null)
        return unit;
      
      unit = new PersistenceUnitManager(this, name, transactionType);
      _persistenceUnitMap.put(name, unit);
    }
    
    registerPersistenceUnit(unit);
      
    return unit;
  }

  /**
   * Adds the URLs for the classpath.
   */
  public void startPersistenceUnits()
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();

    try {
      // jpa/1630
      thread.setContextClassLoader(_classLoader);
      
      InjectManager cdiManager = InjectManager.getCurrent();
      
      cdiManager.updateResources();

      ArrayList<PersistenceUnitManager> pUnitList
        = new ArrayList<PersistenceUnitManager>();

      synchronized (_persistenceUnitMap) {
        pUnitList.addAll(_persistenceUnitMap.values());
      }
      
      for (PersistenceUnitManager pUnit : pUnitList) {
        try {
          pUnit.start();
        } catch (RuntimeException e) {
          if (pUnit.getJtaDataSource() == null
              && pUnit.getNonJtaDataSource() == null) {
            // env/0e22 #4491
            log.warning(e.toString());
            log.log(Level.FINER, e.toString(), e);
          }
          else {
            throw e;
          }
        }
      }
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
  }
  
  public void close()
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();

    try {
      // jpa/1630
      thread.setContextClassLoader(_classLoader);

      ArrayList<PersistenceUnitManager> pUnitList
        = new ArrayList<PersistenceUnitManager>();
      
      synchronized (_persistenceUnitMap) {
        pUnitList.addAll(_persistenceUnitMap.values());
      }
      
      for (PersistenceUnitManager pUnit : pUnitList) {
        pUnit.close();
      }
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
  }

  private void registerPersistenceUnit(PersistenceUnitManager pUnit)
  {
    try {
      InjectManager beanManager = InjectManager.create(_classLoader);
      
      BeanBuilder<EntityManagerFactory> emfFactory;
      emfFactory = beanManager.createBeanFactory(EntityManagerFactory.class);

      emfFactory.qualifier(CurrentLiteral.CURRENT);
      emfFactory.qualifier(Names.create(pUnit.getName()));
      beanManager.addBeanDiscover(emfFactory.singleton(pUnit.getEntityManagerFactoryProxy()));

      if (pUnit.getTransactionType() == PersistenceUnitTransactionType.JTA) {
        log.finer(L.l("register persistent-unit `{0}' with transaction-type JTA", pUnit.getName()));
        BeanBuilder<EntityManager> emFactory;
        emFactory = beanManager.createBeanFactory(EntityManager.class);

        emFactory.qualifier(CurrentLiteral.CURRENT);
        emFactory.qualifier(Names.create(pUnit.getName()));
        beanManager.addBeanDiscover(emFactory.singleton(pUnit.getEntityManagerJtaProxy()));
      } else {
        log.finer(L.l("register persistent-unit `{0}' with transaction-type RESOURCE_LOCAL", pUnit.getName()));
      }

      /*
      factory = manager.createBeanFactory(EntityManager.class);
      factory.binding(CurrentLiteral.CURRENT);
      factory.binding(Names.create(unitName));
      */

      /*
      PersistenceContextComponent pcComp
        = new PersistenceContextComponent(unitName, persistenceContext);
      */

      // manager.addBean(factory.singleton(persistenceContext));
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  void addProviderUnit(ConfigPersistenceUnit unit)
  {
    try {
      Class<?> cl = unit.getProvider();

      if (cl == null)
        cl = getServiceProvider();

      if (cl == null)
        cl = AmberPersistenceProvider.class;

      if (log.isLoggable(Level.CONFIG)) {
        log.config("JPA PersistenceUnit[" + unit.getName() + "] handled by "
            + cl.getName());
      }

      PersistenceProvider provider = (PersistenceProvider) cl.newInstance();

      String unitName = unit.getName();
      Map props = null;

      /*
      synchronized (this) {
        LazyEntityManagerFactory lazyFactory
          = new LazyEntityManagerFactory(unit, provider, props);

        _pendingFactoryList.add(lazyFactory);
      }
      */

      /*
      EntityManagerTransactionProxy persistenceContext
        = new EntityManagerTransactionProxy(this, unitName, props);

      _persistenceContextMap.put(unitName, persistenceContext);

      InjectManager manager = InjectManager.create(_classLoader);
      BeanFactory factory;
      factory = manager.createBeanFactory(EntityManagerFactory.class);
      */
      /*
      EntityManagerFactoryComponent emf
        = new EntityManagerFactoryComponent(manager, this, provider, unit);
      */
/*
      EntityManagerFactoryProxy emf
        = new EntityManagerFactoryProxy(this, unitName);

      factory.binding(CurrentLiteral.CURRENT);
      factory.binding(Names.create(unitName));
      manager.addBean(factory.singleton(emf));

      factory = manager.createBeanFactory(EntityManager.class);
      factory.binding(CurrentLiteral.CURRENT);
      factory.binding(Names.create(unitName));
*/
      /*
      PersistenceContextComponent pcComp
        = new PersistenceContextComponent(unitName, persistenceContext);
      */

//      manager.addBean(factory.singleton(persistenceContext));
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  Class<?> getServiceProvider()
  {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    try {
      Enumeration<URL> e = loader.getResources("META-INF/services/" + PersistenceProvider.class.getName());
      while (e.hasMoreElements()) {
        URL url  = e.nextElement();

        Class<?> providerClass = loadProvider(url);

        if (providerClass != null
            && ! providerClass.equals(AmberPersistenceProvider.class)) {
          return providerClass;
        }
      }
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);
    }

    return null;
  }

  private Class<?> loadProvider(URL url)
  {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    InputStream is = null;

    try {
      is = url.openStream();

      ReadStream in = Vfs.openRead(is);
      String line;
      while ((line = in.readLine()) != null) {
        line = line.trim();

        if (! "".equals(line) && ! line.startsWith("#")) {
          return Class.forName(line, false, loader);
        }
      }
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);
    } finally {
      IoUtil.close(is);
    }

    return null;
  }

  //
  // private

 
  //
  // ScanListener
  //

  /**
   * Since JPA enhances, it is priority 0
   */
  @Override
  public int getScanPriority()
  {
    return 0;
  }

  /**
   * Returns true if the root is a valid scannable root.
   */
  @Override
  public boolean isRootScannable(Path root, String packageRoot)
  {
    if (root.lookup("META-INF/persistence.xml").canRead()
        || (root.getFullPath().endsWith("WEB-INF/classes/")
            && root.lookup("../persistence.xml").canRead())) {
      _pendingRootList.add(root);
    }

    return false;
  }

  @Override
  public ScanClass scanClass(Path root, String packageRoot,
                             String className, int modifiers)
  {
    return null;
  }

  @Override
  public boolean isScanMatchAnnotation(CharBuffer annotationName)
  {
    return false;
  }

  /**
   * Callback to note the class matches
   */
  @Override
  public void classMatchEvent(EnvironmentClassLoader loader, 
                              Path root,
                              String className)
  {
  }

  //
  // EnvironmentListener
  //

  /**
   * Handles the environment config phase
   */
  @Override
  public void environmentConfigureEnhancer(EnvironmentClassLoader loader)
  {
    configurePersistenceRoots();
    
    // env/0h31
    startPersistenceUnits();
  }

  /**
   * Handles the environment config phase
   */
  @Override
  public void environmentConfigure(EnvironmentClassLoader loader)
  {
    configurePersistenceRoots();
  }

  /**
   * Handles the environment config phase
   */
  @Override
  public void environmentBind(EnvironmentClassLoader loader)
  {
    configurePersistenceRoots();
  }

  /**
   * Handles the case where the environment is starting (after init).
   */
  @Override
  public void environmentStart(EnvironmentClassLoader loader)
  {
    start();
  }

  /**
   * Handles the case where the environment is stopping
   */
  @Override
  public void environmentStop(EnvironmentClassLoader loader)
  {
    close();
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _classLoader.getId() + "]";
  }

  class LazyEntityManagerFactory {
    private final ConfigPersistenceUnit _unit;
    private final PersistenceProvider _provider;
    private final Map _props;

    LazyEntityManagerFactory(ConfigPersistenceUnit unit,
                             PersistenceProvider provider, Map props)
    {
      _unit = unit;
      _provider = provider;
      _props = props;
    }

    void init()
    {
      /*
      synchronized (ManagerPersistence.this) {
        String unitName = _unit.getName();

        EntityManagerFactory factory = _factoryMap.get(unitName);

        if (factory == null) {
          factory = _provider.createContainerEntityManagerFactory(_unit, _props);

          if (factory == null)
            throw new ConfigException(L.l(
                "'{0}' must return an EntityManagerFactory",
                _provider.getClass().getName()));

          if (log.isLoggable(Level.FINE)) {
            log.fine(L.l("{0} creating persistence unitName={1} created with provider '{2}'",
                         this, unitName, _provider.getClass().getName()));
          }

          _factoryMap.put(unitName, factory);
        }
      }
      */
    }
  }

}
