/**
 * This file is part of Eclipse Steady.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
 */
package org.eclipse.steady.java.monitor;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.logging.log4j.Logger;
import org.eclipse.steady.core.util.CoreConfiguration;
import org.eclipse.steady.java.JarAnalyzer;
import org.eclipse.steady.java.JarWriter;
import org.eclipse.steady.java.JavaId;
import org.eclipse.steady.java.JavaMethodId;
import org.eclipse.steady.java.monitor.touch.ConstructIdUtil;
import org.eclipse.steady.java.monitor.trace.ConstructUsage;
import org.eclipse.steady.shared.util.VulasConfiguration;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;

/**
 * Uses the {@link ClassVisitor} to dynamically instrument Java constructors and methods
 * during the class loading process. When executed, the injected code will call the
 * {@link ExecutionMonitor} to collect and upload runtime information to the Vulas backend.
 * <p>
 * The name has been chosen to contrast with static instrumentation, which can be done
 * with help of the Vulas Maven plugin (goal vulas:instr).
 */
public class DynamicTransformer implements ClassFileTransformer {

  // ====================================== STATIC MEMBERS

  private static final Logger log =
      org.apache.logging.log4j.LogManager.getLogger(DynamicTransformer.class);

  private static DynamicTransformer instance = null;

  // ====================================== INSTANCE MEMBERS

  private String id = Double.toString(Math.random());

  private LoaderHierarchy loaderHierarchy = new LoaderHierarchy();

  private InstrumentationControl instrControl = null;

  /**
   * Determines whether classes will be instrumented or not.
   * Checked in {@link DynamicTransformer#transform(ClassLoader, String, Class, ProtectionDomain, byte[])}.
   * Will be changed to true only after a successful instantiation, otherwise
   * the transform method will called to early (which results in StackOverflow problems).
   */
  private boolean transformationEnabled = false;

  private DynamicTransformer() throws IllegalStateException {
    this.instrControl = InstrumentationControl.getInstance(this.getClass().getSimpleName());

    try {
      if (!CoreConfiguration.existsInBackend(CoreConfiguration.getAppContext()))
        throw new IllegalStateException(
            "Application " + CoreConfiguration.getAppContext() + " does not exist in backend");
    } catch (ConfigurationException e) {
      throw new IllegalStateException("Error while reading configuration: " + e.getMessage());
    }

    // Freeze a couple of classes
    this.freezeClasses();
  }

  // ====================================== INSTANCE METHODS

  /**
   * Called during the construction in order to have some classes frozen.
   * @return
   */
  private final void freezeClasses() {
    try {
      final JavaMethodId jmi = JavaId.parseMethodQName("com.sap.Test.test()");
      final ConstructUsage cu = new ConstructUsage(jmi, null, -1);
      final Loader l = new Loader(this.getClass().getClassLoader());
      final Configuration cfg = VulasConfiguration.getGlobal().getConfiguration();
      ConstructIdUtil.getInstance();
      final JarWriter jw =
          new JarWriter(
              Paths.get(
                  DynamicTransformer.class
                      .getClassLoader()
                      .getResource(DynamicTransformer.class.getName().replace('.', '/') + ".class")
                      .toString()));
      final JarAnalyzer ja = new JarAnalyzer();
      ja.analyze(
          Paths.get(
                  DynamicTransformer.class
                      .getClassLoader()
                      .getResource(DynamicTransformer.class.getName().replace('.', '/') + ".class")
                      .toString())
              .toFile());
    }
    // Getting an exception does not matter in the context of freezing some classes
    catch (Exception e) {
      ;
    }
  }

  /**
   * <p>isTransformationEnabled.</p>
   *
   * @return a boolean.
   */
  public boolean isTransformationEnabled() {
    return transformationEnabled;
  }

  /**
   * <p>Setter for the field <code>transformationEnabled</code>.</p>
   *
   * @param transformationEnabled a boolean.
   */
  public void setTransformationEnabled(boolean transformationEnabled) {
    this.transformationEnabled = transformationEnabled;
  }

  /**
   * <p>Getter for the field <code>loaderHierarchy</code>.</p>
   *
   * @return a {@link org.eclipse.steady.java.monitor.LoaderHierarchy} object.
   */
  public LoaderHierarchy getLoaderHierarchy() {
    return this.loaderHierarchy;
  }

  /**
   * Adds instrumentation code to all constructors and methods of all classes (thereby considering certain
   * blacklists and whitelists read from the configuration file).
   * <p>
   * The method is called by the JRE class loading process and returns the instrumented bytecode for a given
   * class.
   *
   * @return the instrumented bytecode
   * @param loader a {@link java.lang.ClassLoader} object.
   * @param className a {@link java.lang.String} object.
   * @param classBeingRedefined a {@link java.lang.Class} object.
   * @param protectionDomain a {@link java.security.ProtectionDomain} object.
   * @param classfileBuffer an array of {@link byte} objects.
   * @throws java.lang.instrument.IllegalClassFormatException if any.
   */
  public byte[] transform(
      ClassLoader loader,
      String className,
      Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer)
      throws IllegalClassFormatException {

    byte[] byteCode = classfileBuffer;

    // All methods of current class
    Loader l = null;
    CtClass c = null;
    ClassVisitor cv = null;

    final String loader_classname = loader.getClass().getName();
    final String dot_classname = className.replace('/', '.');

    // 07.08.2015, HP: Added in order to load class definition for stacktrace transformation
    if (loader.getParent() != null) // && loader!=null
    this.loaderHierarchy.add(loader);

    // We are not interested in instrumenting classes in the following cases:
    // - loader==null || loader.getParent()==null: The class in question is loaded by the bootstrap
    // loader, which loads class definitions from JRE_HOME/lib/ext.
    // - loader is instance of sun.reflect.DelegatingClassLoader (created for the optimization of
    // java.lang.reflect calls) or javax.management.remote.rmi.NoCallStackClassLoader (?)
    // - classname is blacklisted
    if (loader.getParent() != null) { // && // && loader!=null

      // Let's add the loader to the class loader hierarchy
      l = this.loaderHierarchy.add(loader);

      // Is the tracer (already) supposed to instrument?
      // If not we spare us the expensive instrumentation (but assume it happened already before,
      // for all JARs, so that the callback happens nevertheless)
      if (this.isTransformationEnabled()) {

        // Blacklisted class according to the configuration parameters
        // "instr.blacklist.classes.jre/custom"?
        final boolean is_blacklisted_class = this.instrControl.isBlacklistedClass(dot_classname);

        // Class is blacklisted
        if (!is_blacklisted_class) {
          try {
            ClassPool cp = l.getClassPool(); // ClassPool.getDefault();
            c = cp.get(dot_classname);

            // Blacklisted JAR according to the configuration parameters
            // "instr.blacklist.jars/dirs"?
            final boolean is_blacklisted_jar = this.instrControl.isBlacklistedJar(c.getURL());

            // Instrument methods and constructors of classes (but not interfaces)
            if (!is_blacklisted_jar && !c.isInterface()) {
              cv = new ClassVisitor(c);
              if (!cv.isInstrumented()) {
                cv.visitMethods(true);
                cv.visitConstructors(true);
                cv.finalizeInstrumentation();
                byteCode = cv.getBytecode();
                this.instrControl.updateInstrumentationStatistics(
                    cv.getJavaId(), Boolean.valueOf(true));
                DynamicTransformer.log.debug("Class [" + dot_classname + "] now instrumented");
              } else {
                this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), null);
                DynamicTransformer.log.debug("Class [" + dot_classname + "] already instrumented");
              }
            }
          } catch (IOException ioe) {
            DynamicTransformer.log.error(
                "I/O exception while instrumenting class ["
                    + dot_classname
                    + "]: "
                    + ioe.getMessage());
            if (cv != null) {
              this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), false);
            }
          } catch (CannotCompileException cce) {
            DynamicTransformer.log.warn(
                "Cannot compile instrumented class [" + dot_classname + "]: " + cce.getMessage());
            if (cv != null) {
              this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), false);
            }
          }
          // Covers the following problems with Javassist and Java 8: "java.io.IOException: invalid
          // constant type: 15"
          catch (Exception e) {
            DynamicTransformer.log.warn(
                e.getClass().getName()
                    + " occured while instrumenting class ["
                    + dot_classname
                    + "]: "
                    + e.getMessage());
            if (cv != null) {
              this.instrControl.updateInstrumentationStatistics(cv.getJavaId(), false);
            }
          }
        }
      }
    }
    return byteCode;
  }

  /**
   * <p>toString.</p>
   *
   * @return a {@link java.lang.String} object.
   */
  public String toString() {
    final StringBuffer b = new StringBuffer();
    b.append("DynamicTransformer [id=").append(this.id);
    b.append(", instrumentation enabled=").append(this.isTransformationEnabled());
    b.append("]");
    return b.toString();
  }

  // ====================================== STATIC METHODS

  /**
   * Singleton method: Creates (if necessary) and returns the single instance that can be created for this class.
   *
   * @return a {@link org.eclipse.steady.java.monitor.DynamicTransformer} object.
   */
  public static synchronized DynamicTransformer getInstance() {
    if (DynamicTransformer.instance == null) {
      // Disable trace collection during the instantiation process. As we use a couple of OSS
      // components
      // ourselves, we may end up in an endless loop and StackOverflow exceptions otherwise
      // DynamicTransformer.TRACE_ENABLED = false;

      DynamicTransformer.instance = new DynamicTransformer();

      // Now that the instance has been created, we enable tracing again
      instance.setTransformationEnabled(true);
    }
    return DynamicTransformer.instance;
  }

  /**
   * Returns true if the Singleton has been created.
   *
   * @return a boolean.
   */
  public static synchronized boolean isInstantiated() {
    return DynamicTransformer.instance != null;
  }

  /**
   * <p>premain.</p>
   *
   * @param agentArgs a {@link java.lang.String} object.
   * @param inst a {@link java.lang.instrument.Instrumentation} object.
   */
  public static void premain(String agentArgs, Instrumentation inst) {
    // Create monitor, which will register upload scheduler and start goal execution
    final ExecutionMonitor m = ExecutionMonitor.getInstance();

    // Create and register transformer, which will inject the byte code using instrumentors
    final DynamicTransformer t = DynamicTransformer.getInstance();
    inst.addTransformer(t);

    DynamicTransformer.log.info(t + " registered via JVM option -javaagent");
  }
}
