/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.phoenix.expression.function;

import static org.apache.phoenix.query.QueryServices.DYNAMIC_JARS_DIR_KEY;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.DynamicClassLoader;
import org.apache.hadoop.io.WritableUtils;
import org.apache.phoenix.compile.KeyPart;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.visitor.ExpressionVisitor;
import org.apache.phoenix.parse.PFunction;
import org.apache.phoenix.schema.PName;
import org.apache.phoenix.schema.PNameFactory;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.schema.types.PDataType;

import org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
import org.apache.phoenix.thirdparty.com.google.common.collect.MapMaker;

public class UDFExpression extends ScalarFunction {

  private static Configuration config = HBaseConfiguration.create();

  private static final ConcurrentMap<PName, DynamicClassLoader> tenantIdSpecificCls =
    new MapMaker().concurrencyLevel(3).weakValues().makeMap();

  private static final ConcurrentMap<String, DynamicClassLoader> pathSpecificCls =
    new MapMaker().concurrencyLevel(3).weakValues().makeMap();

  private PName tenantId;
  private String functionClassName;
  private String jarPath;
  private ScalarFunction udfFunction;

  public UDFExpression() {
  }

  public UDFExpression(List<Expression> children, PFunction functionInfo) {
    super(children);
    this.tenantId =
      functionInfo.getTenantId() == null ? PName.EMPTY_NAME : functionInfo.getTenantId();
    this.functionClassName = functionInfo.getClassName();
    this.jarPath = functionInfo.getJarPath();
    constructUDFFunction();
  }

  public UDFExpression(List<Expression> children, PName tenantId, String functionClassName,
    String jarPath, ScalarFunction udfFunction) {
    super(children);
    this.tenantId = tenantId;
    this.functionClassName = functionClassName;
    this.jarPath = jarPath;
    if (udfFunction != null) {
      this.udfFunction = udfFunction;
    } else {
      constructUDFFunction();
    }
  }

  @Override
  public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
    return udfFunction.evaluate(tuple, ptr);
  }

  @Override
  public <T> T accept(ExpressionVisitor<T> visitor) {
    return udfFunction.accept(visitor);
  }

  @Override
  public PDataType getDataType() {
    return udfFunction.getDataType();
  }

  @Override
  public String getName() {
    return udfFunction.getName();
  }

  @Override
  public OrderPreserving preservesOrder() {
    return udfFunction.preservesOrder();
  }

  @Override
  public KeyPart newKeyPart(KeyPart childPart) {
    return udfFunction.newKeyPart(childPart);
  }

  @Override
  public int getKeyFormationTraversalIndex() {
    return udfFunction.getKeyFormationTraversalIndex();
  }

  public PName getTenantId() {
    return tenantId;
  }

  public String getFunctionClassName() {
    return functionClassName;
  }

  public String getJarPath() {
    return jarPath;
  }

  public ScalarFunction getUdfFunction() {
    return udfFunction;
  }

  @Override
  public void write(DataOutput output) throws IOException {
    super.write(output);
    WritableUtils.writeString(output, tenantId.getString());
    WritableUtils.writeString(output, this.functionClassName);
    if (this.jarPath == null) {
      WritableUtils.writeString(output, "");
    } else {
      WritableUtils.writeString(output, this.jarPath);
    }
  }

  @Override
  public void readFields(DataInput input) throws IOException {
    super.readFields(input);
    this.tenantId = PNameFactory.newName(WritableUtils.readString(input));
    this.functionClassName = WritableUtils.readString(input);
    String str = WritableUtils.readString(input);
    this.jarPath = str.length() == 0 ? null : str;
    constructUDFFunction();
  }

  private void constructUDFFunction() {
    try {
      DynamicClassLoader classLoader = getClassLoader(this.tenantId, this.jarPath);
      Class<?> clazz = classLoader.loadClass(this.functionClassName);
      Constructor<?> constructor = clazz.getConstructor(List.class);
      udfFunction = (ScalarFunction) constructor.newInstance(this.children);
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
      | InstantiationException | IllegalAccessException | IllegalArgumentException
      | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  public static DynamicClassLoader getClassLoader(final PName tenantId, final String jarPath) {
    DynamicClassLoader cl = tenantIdSpecificCls.get(tenantId);
    Path parent = null;
    if (cl != null) return cl;
    if (jarPath != null && !jarPath.isEmpty()) {
      cl = pathSpecificCls.get(jarPath);
      if (cl != null) return cl;
      parent = getPathForParent(jarPath);
    }
    // Parse the DYNAMIC_JARS_DIR_KEY value as a Path if it's present in the configuration
    Path allowedDynamicJarsPath =
      config.get(DYNAMIC_JARS_DIR_KEY) != null ? new Path(config.get(DYNAMIC_JARS_DIR_KEY)) : null;
    // The case jarPath is not provided, or it is provided and the jar is inside
    // hbase.dynamic.jars.dir
    if (
      jarPath == null || jarPath.isEmpty()
        || (allowedDynamicJarsPath != null && parent != null
          && parent.equals(allowedDynamicJarsPath))
    ) {
      cl = tenantIdSpecificCls.get(tenantId);
      if (cl == null) {
        cl = new DynamicClassLoader(config, UDFExpression.class.getClassLoader());
      }
      // Cache class loader as a weak value, will be GC'ed when no reference left
      DynamicClassLoader prev = tenantIdSpecificCls.putIfAbsent(tenantId, cl);
      if (prev != null) {
        cl = prev;
      }
      return cl;
    } else {
      // The case jarPath is provided as not part of DYNAMIC_JARS_DIR_KEY
      // As per PHOENIX-4231, DYNAMIC_JARS_DIR_KEY is the only place where loading a udf jar is
      // allowed
      throw new SecurityException(
        "Loading jars from " + jarPath + " is not allowed. The only location that is allowed is "
          + config.get(DYNAMIC_JARS_DIR_KEY));
    }
  }

  public static Path getPathForParent(String jarPath) {
    Path path = new Path(jarPath);
    if (jarPath.endsWith(".jar")) {
      return path.getParent();
    }
    return path;
  }

  @VisibleForTesting
  public static void setConfig(Configuration conf) {
    config = conf;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof UDFExpression)) {
      return false;
    }
    UDFExpression that = (UDFExpression) obj;
    if (!this.udfFunction.getName().equals(that.udfFunction.getName())) {
      return false;
    }
    if (!this.udfFunction.getChildren().equals(that.udfFunction.getChildren())) {
      return false;
    }
    if (!functionClassName.equals(that.functionClassName)) {
      return false;
    }
    if (!jarPath.equals(that.jarPath)) {
      return false;
    }
    return true;
  }
}
