/*
 * 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.config.inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.enterprise.inject.spi.AnnotatedType;

import com.caucho.config.program.ConfigProgram;
import com.caucho.config.reflect.ReflectionAnnotatedFactory;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.loader.EnvironmentClassLoader;
import com.caucho.loader.enhancer.ScanClass;
import com.caucho.loader.enhancer.ScanListener;
import com.caucho.util.CharBuffer;
import com.caucho.vfs.Path;

/**
 * The web beans container for a given environment.
 */
class InjectScanManager implements ScanListener {
  private static final Logger log
    = Logger.getLogger(InjectScanManager.class.getName());

  private InjectManager _injectManager;

  private final HashMap<Path, ScanRootContext> _scanRootMap
    = new HashMap<Path, ScanRootContext>();

  private final ArrayList<ScanRootContext> _pendingScanRootList
    = new ArrayList<ScanRootContext>();

  private final ConcurrentHashMap<String, InjectScanClass> _scanClassMap
    = new ConcurrentHashMap<String, InjectScanClass>();

  private final ConcurrentHashMap<NameKey, AnnType> _annotationMap
    = new ConcurrentHashMap<NameKey, AnnType>();
  
  private final ArrayList<InjectScanClass> _immediateResourceList
    = new ArrayList<InjectScanClass>();
  
  private NameKey _nameKey = new NameKey();

  private boolean _isCustomExtension;

  private ArrayList<InjectScanClass> _pendingScanClassList
    = new ArrayList<InjectScanClass>();

  InjectScanManager(InjectManager injectManager)
  {
    _injectManager = injectManager;
  }

  /**
   * Returns the injection manager.
   */
  public InjectManager getInjectManager()
  {
    return _injectManager;
  }

  /**
   * True if a custom extension exists.
   */
  public void setIsCustomExtension(boolean isCustomExtension)
  {
    _isCustomExtension = isCustomExtension;

    if (isCustomExtension) {
      for (InjectScanClass scanClass : _scanClassMap.values()) {
        scanClass.register();
      }
    }
  }

  public boolean isCustomExtension()
  {
    return _isCustomExtension;
  }

  public ArrayList<ScanRootContext> getPendingScanRootList()
  {
    ArrayList<ScanRootContext> contextList
      = new ArrayList<ScanRootContext>(_pendingScanRootList);

    _pendingScanRootList.clear();

    return contextList;
  }

  public boolean isPending()
  {
    return _pendingScanClassList.size() > 0 || _pendingScanRootList.size() > 0;
  }

  public void addDiscoveredClass(InjectScanClass injectScanClass)
  {
    if (! _pendingScanClassList.contains(injectScanClass)) {
      _pendingScanClassList.add(injectScanClass);
    }
  }

  /**
   * discovers pending beans.
   */
  public void discover()
  {
    ArrayList<InjectScanClass> pendingScanClassList
      = new ArrayList<InjectScanClass>(_pendingScanClassList);

    _pendingScanClassList.clear();

    for (InjectScanClass scanClass : pendingScanClassList) {
      getInjectManager().discoverBean(scanClass);
    }
  }

  //
  // ScanListener

  /**
   * Since CDI doesn't enhance, it's priority 1
   */
  @Override
  public int getScanPriority()
  {
    return 1;
  }

  @Override
  public boolean isRootScannable(Path root, String packageRoot)
  {
    ScanRootContext context = _scanRootMap.get(root);
    List<Path> beansXmlOverride = _injectManager.getBeansXmlOverride(root);

    Path scanRoot = root;

    if (packageRoot != null) {
      scanRoot = scanRoot.lookup(packageRoot.replace('.', '/'));
    }
    
    if (beansXmlOverride == null) {
      // TODO Should resin-beans.xml be included in this check?
      if (packageRoot != null) {
        if (! scanRoot.lookup("beans.xml").canRead()
            && ! scanRoot.lookup("META-INF/beans.xml").canRead()) {
          return false;
        }
      } else if (! (root.lookup("META-INF/beans.xml").canRead() 
                    || (root.getFullPath().endsWith("WEB-INF/classes/")
                        && root.lookup("../beans.xml").canRead()))) {
        return false;
      }
    }

    if (context == null) {
      context = new ScanRootContext(scanRoot, packageRoot);
      _scanRootMap.put(root, context);
      _pendingScanRootList.add(context);
    }

    if (context.isScanComplete())
      return false;
    else {
      if (log.isLoggable(Level.FINER))
        log.finer("CanDI scanning " + root.getURL());

      context.setScanComplete(true);
      return true;
    }
  }

  /**
   * Checks if the class can be a simple class
   */
  @Override
  public ScanClass scanClass(Path root, 
                             String packageRoot,
                             String className,
                             int modifiers)
  {
    // ioc/0j0k - package private allowed

    if (Modifier.isPrivate(modifiers))
      return null;
    else {
      InjectScanClass scanClass = createScanClass(className);

      scanClass.setScanClass();

      return scanClass;
    }
  }

  InjectScanClass getScanClass(String className)
  {
    return _scanClassMap.get(className);
  }

  InjectScanClass createScanClass(String className)
  {
    InjectScanClass scanClass = _scanClassMap.get(className);

    if (scanClass == null) {
      scanClass = new InjectScanClass(className, this);
      InjectScanClass oldScanClass;
      oldScanClass = _scanClassMap.putIfAbsent(className, scanClass);

      if (oldScanClass != null)
        scanClass = oldScanClass;
    }

    return scanClass;
  }

  /**
   * Loads an annotation for scanning.
   */
  public AnnType loadAnnotation(char[] buffer, int offset, int length)
    throws ClassNotFoundException
  {
    NameKey key = _nameKey;
    
    key.init(buffer, offset, length); // new NameKey(buffer, offset, length);

    AnnType annType = _annotationMap.get(key);

    if (annType != null)
      return annType;

    ClassLoader loader = getInjectManager().getClassLoader();

    String className = new String(buffer, offset, length);

    Class<?> cl = Class.forName(className, false, loader);

    annType = new AnnType(cl);

    _annotationMap.put(key.dup(), annType);

    return annType;
  }

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

  @Override
  public void classMatchEvent(EnvironmentClassLoader loader, 
                              Path root,
                              String className)
  {
  }

  public void addImmediateResource(InjectScanClass injectScanClass)
  {
    ClassLoader classLoader = _injectManager.getClassLoader();

    if (classLoader instanceof DynamicClassLoader) {
      DynamicClassLoader dynLoader = (DynamicClassLoader) classLoader;
      ClassLoader tmpLoader = dynLoader.getNewTempClassLoader();
      
      Thread thread = Thread.currentThread();
      ClassLoader oldLoader = thread.getContextClassLoader();
      
      try {
        thread.setContextClassLoader(dynLoader);
        
        Class<?> cl = tmpLoader.loadClass(injectScanClass.getClassName());
        
        AnnotatedType<?> annType
          = ReflectionAnnotatedFactory.introspectType(cl);

        for (Annotation ann : cl.getAnnotations()) {
          InjectionPointHandler handler
            = _injectManager.getInjectionPointHandler(ann.annotationType());

          if (handler != null) {
            ConfigProgram program = handler.introspectType(annType);
            
            if (program != null)
              program.inject(null, null);
          }
        }
      } catch (Exception e) {
        log.log(Level.FINE, e.toString(), e);
      } finally {
        thread.setContextClassLoader(oldLoader);
      }
    }
  }

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

  static class NameKey {
    private char[] _buffer;
    private int _offset;
    private int _length;

    NameKey(char[] buffer, int offset, int length)
    {
      init(buffer, offset, length);
    }
    
    NameKey()
    {
    }

    void init(char[] buffer, int offset, int length)
    {
      _buffer = buffer;
      _offset = offset;
      _length = length;
    }

    public NameKey dup()
    {
      char[] buffer = new char[_length];

      System.arraycopy(_buffer, _offset, buffer, 0, _length);

      //_buffer = buffer;
      //_offset = 0;

      return new NameKey(buffer, 0, _length);
    }

    public int hashCode()
    {
      char[] buffer = _buffer;
      int offset = _offset;
      int length = _length;
      int hash = length;

      for (length--; length >= 0; length--) {
        char value = buffer[offset + length];

        hash = 65521 * hash + value;
      }

      return hash;
    }

    public boolean equals(Object o)
    {
      if (!(o instanceof NameKey))
        return false;

      NameKey key = (NameKey) o;

      if (_length != key._length)
        return false;

      char[] bufferA = _buffer;
      char[] bufferB = key._buffer;

      int offsetA = _offset;
      int offsetB = key._offset;

      for (int i = _length - 1; i >= 0; i--) {
        if (bufferA[offsetA + i] != bufferB[offsetB + i])
          return false;
      }

      return true;
    }

    @Override
    public String toString()
    {
      return this.getClass().getSimpleName() + "[" + new String(_buffer,
                                                                _offset,
                                                                _length);
    }
  }

  static class AnnType {
    private Class<?> _type;
    private Annotation[] _annotations;

    AnnType(Class<?> type)
    {
      _type = type;
    }

    public Class<?> getType()
    {
      return _type;
    }

    public Annotation[] getAnnotations()
    {
      if (_annotations == null)
        _annotations = _type.getAnnotations();

      return _annotations;
    }
  }
}