/*
 * 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.drill.exec.expr.fn.registry;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;

import org.apache.drill.common.AutoCloseables.Closeable;
import org.apache.drill.common.concurrent.AutoCloseableLock;
import org.apache.drill.exec.expr.fn.DrillFuncHolder;

import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Function registry holder stores function implementations by jar name, function name.
 * Contains two maps that hold data by jars and functions respectively.
 * Jars map contains each jar as a key and map of all its functions with collection of function signatures as value.
 * Functions map contains function name as key and map of its signatures and function holder as value.
 * All maps and collections used are concurrent to guarantee memory consistency effects.
 * Such structure is chosen to achieve maximum speed while retrieving data by jar or by function name,
 * since we expect infrequent registry changes.
 * Holder is designed to allow concurrent reads and single writes to keep data consistent.
 * This is achieved by {@link ReadWriteLock} implementation usage.
 * Holder has number version which indicates remote function registry version number it is in sync with.
 *
 * Structure example:
 *
 * JARS
 * built-in   -> upper          -> upper(VARCHAR-REQUIRED)
 *            -> lower          -> lower(VARCHAR-REQUIRED)
 *
 * First.jar  -> upper          -> upper(VARCHAR-OPTIONAL)
 *            -> custom_upper   -> custom_upper(VARCHAR-REQUIRED)
 *                              -> custom_upper(VARCHAR-OPTIONAL)
 *
 * Second.jar -> lower          -> lower(VARCHAR-OPTIONAL)
 *            -> custom_upper   -> custom_upper(VARCHAR-REQUIRED)
 *                              -> custom_upper(VARCHAR-OPTIONAL)
 *
 * FUNCTIONS
 * upper        -> upper(VARCHAR-REQUIRED)        -> function holder for upper(VARCHAR-REQUIRED)
 *              -> upper(VARCHAR-OPTIONAL)        -> function holder for upper(VARCHAR-OPTIONAL)
 *
 * lower        -> lower(VARCHAR-REQUIRED)        -> function holder for lower(VARCHAR-REQUIRED)
 *              -> lower(VARCHAR-OPTIONAL)        -> function holder for lower(VARCHAR-OPTIONAL)
 *
 * custom_upper -> custom_upper(VARCHAR-REQUIRED) -> function holder for custom_upper(VARCHAR-REQUIRED)
 *              -> custom_upper(VARCHAR-OPTIONAL) -> function holder for custom_upper(VARCHAR-OPTIONAL)
 *
 * custom_lower -> custom_lower(VARCHAR-REQUIRED) -> function holder for custom_lower(VARCHAR-REQUIRED)
 *              -> custom_lower(VARCHAR-OPTIONAL) -> function holder for custom_lower(VARCHAR-OPTIONAL)
 *
 * where
 * First.jar is jar name represented by String
 * upper is function name represented by String
 * upper(VARCHAR-REQUIRED) is signature name represented by String which consist of function name, list of input parameters
 * function holder for upper(VARCHAR-REQUIRED) is {@link DrillFuncHolder} initiated for each function.
 *
 */
public class FunctionRegistryHolder {

  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(FunctionRegistryHolder.class);

  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  private final AutoCloseableLock readLock = new AutoCloseableLock(readWriteLock.readLock());
  private final AutoCloseableLock writeLock = new AutoCloseableLock(readWriteLock.writeLock());
  // remote function registry number, it is in sync with
  private long version;

  // jar name, Map<function name, Queue<function signature>
  private final Map<String, Map<String, Queue<String>>> jars;

  // function name, Map<function signature, function holder>
  private final Map<String, Map<String, DrillFuncHolder>> functions;

  public FunctionRegistryHolder() {
    this.functions = Maps.newConcurrentMap();
    this.jars = Maps.newConcurrentMap();
  }

  /**
   * This is read operation, so several users at a time can get this data.
   * @return local function registry version number
   */
  public long getVersion() {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      return version;
    }
  }

  /**
   * Adds jars to the function registry.
   * If jar with the same name already exists, it and its functions will be removed.
   * Then jar will be added to {@link #jars}
   * and each function will be added using {@link #addFunctions(Map, List)}.
   * Registry version is updated with passed version if all jars were added successfully.
   * This is write operation, so one user at a time can call perform such action,
   * others will wait till first user completes his action.
   *
   * @param newJars jars and list of their function holders, each contains function name, signature and holder
   */
  public void addJars(Map<String, List<FunctionHolder>> newJars, long version) {
    try (@SuppressWarnings("unused") Closeable lock = writeLock.open()) {
      for (Map.Entry<String, List<FunctionHolder>> newJar : newJars.entrySet()) {
        String jarName = newJar.getKey();
        removeAllByJar(jarName);
        Map<String, Queue<String>> jar = Maps.newConcurrentMap();
        jars.put(jarName, jar);
        addFunctions(jar, newJar.getValue());
      }
      this.version = version;
    }
  }

  /**
   * Removes jar from {@link #jars} and all associated with jar functions from {@link #functions}
   * This is write operation, so one user at a time can call perform such action,
   * others will wait till first user completes his action.
   *
   * @param jarName jar name to be removed
   */
  public void removeJar(String jarName) {
    try (@SuppressWarnings("unused") Closeable lock = writeLock.open()) {
      removeAllByJar(jarName);
    }
  }

  /**
   * Retrieves list of all jars name present in {@link #jars}
   * This is read operation, so several users can get this data.
   *
   * @return list of all jar names
   */
  public List<String> getAllJarNames() {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      return Lists.newArrayList(jars.keySet());
    }
  }

  /**
   * Retrieves all function names associated with the jar from {@link #jars}.
   * Returns empty list if jar is not registered.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param jarName jar name
   * @return list of functions names associated from the jar
   */
  public List<String> getFunctionNamesByJar(String jarName) {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()){
      Map<String, Queue<String>> functions = jars.get(jarName);
      return functions == null ? Lists.<String>newArrayList() : Lists.newArrayList(functions.keySet());
    }
  }

  /**
   * Returns list of functions with list of function holders for each functions.
   * Uses guava {@link ListMultimap} structure to return data.
   * If no functions present, will return empty {@link ListMultimap}.
   * If version holder is not null, updates it with current registry version number.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param version version holder
   * @return all functions which their holders
   */
  public ListMultimap<String, DrillFuncHolder> getAllFunctionsWithHolders(AtomicLong version) {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      if (version != null) {
        version.set(this.version);
      }
      ListMultimap<String, DrillFuncHolder> functionsWithHolders = ArrayListMultimap.create();
      for (Map.Entry<String, Map<String, DrillFuncHolder>> function : functions.entrySet()) {
        functionsWithHolders.putAll(function.getKey(), Lists.newArrayList(function.getValue().values()));
      }
      return functionsWithHolders;
    }
  }

  /**
   * Returns list of functions with list of function holders for each functions without version number.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @return all functions which their holders
   */
  public ListMultimap<String, DrillFuncHolder> getAllFunctionsWithHolders() {
    return getAllFunctionsWithHolders(null);
  }

  /**
   * Returns list of functions with list of function signatures for each functions.
   * Uses guava {@link ListMultimap} structure to return data.
   * If no functions present, will return empty {@link ListMultimap}.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @return all functions which their signatures
   */
  public ListMultimap<String, String> getAllFunctionsWithSignatures() {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      ListMultimap<String, String> functionsWithSignatures = ArrayListMultimap.create();
      for (Map.Entry<String, Map<String, DrillFuncHolder>> function : functions.entrySet()) {
        functionsWithSignatures.putAll(function.getKey(), Lists.newArrayList(function.getValue().keySet()));
      }
      return functionsWithSignatures;
    }
  }

  /**
   * Returns all function holders associated with function name.
   * If function is not present, will return empty list.
   * If version holder is not null, updates it with current registry version number.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param functionName function name
   * @param version version holder
   * @return list of function holders
   */
  public List<DrillFuncHolder> getHoldersByFunctionName(String functionName, AtomicLong version) {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      if (version != null) {
        version.set(this.version);
      }
      Map<String, DrillFuncHolder> holders = functions.get(functionName);
      return holders == null ? Lists.<DrillFuncHolder>newArrayList() : Lists.newArrayList(holders.values());
    }
  }

  /**
   * Returns all function holders associated with function name without version number.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param functionName function name
   * @return list of function holders
   */
  public List<DrillFuncHolder> getHoldersByFunctionName(String functionName) {
    return getHoldersByFunctionName(functionName, null);
  }

  /**
   * Checks is jar is present in {@link #jars}.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param jarName jar name
   * @return true if jar exists, else false
   */
  public boolean containsJar(String jarName) {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      return jars.containsKey(jarName);
    }
  }

  /**
   * Returns quantity of functions stored in {@link #functions}.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @return quantity of functions
   */
  public int functionsSize() {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      return functions.size();
    }
  }

  /**
   * Looks which jar in {@link #jars} contains passed function signature.
   * First looks by function name and if found checks if such function has passed function signature.
   * Returns jar name if found matching function signature, else null.
   * This is read operation, so several users can perform this operation at the same time.
   *
   * @param functionName function name
   * @param functionSignature function signature
   * @return jar name
   */
  public String getJarNameByFunctionSignature(String functionName, String functionSignature) {
    try (@SuppressWarnings("unused") Closeable lock = readLock.open()) {
      for (Map.Entry<String, Map<String, Queue<String>>> jar : jars.entrySet()) {
        Queue<String> functionSignatures = jar.getValue().get(functionName);
        if (functionSignatures != null && functionSignatures.contains(functionSignature)) {
          return jar.getKey();
        }
      }
    }
    return null;
  }

  /**
   * Adds all function names and signatures to passed jar,
   * adds all function names, their signatures and holders to {@link #functions}.
   *
   * @param jar jar where function to be added
   * @param newFunctions collection of function holders, each contains function name, signature and holder.
   */
  private void addFunctions(Map<String, Queue<String>> jar, List<FunctionHolder> newFunctions) {
    for (FunctionHolder function : newFunctions) {
      final String functionName = function.getName();
      Queue<String> jarFunctions = jar.get(functionName);
      if (jarFunctions == null) {
        jarFunctions = Queues.newConcurrentLinkedQueue();;
        jar.put(functionName, jarFunctions);
      }
      final String functionSignature = function.getSignature();
      jarFunctions.add(functionSignature);

      Map<String, DrillFuncHolder> signatures = functions.get(functionName);
      if (signatures == null) {
        signatures = Maps.newConcurrentMap();
        functions.put(functionName, signatures);
      }
      signatures.put(functionSignature, function.getHolder());
    }
  }

  /**
   * Removes jar from {@link #jars} and all associated with jars functions from {@link #functions}
   * Since each jar is loaded with separate class loader before
   * removing we need to close class loader to release opened connection to jar.
   * All jar functions have the same class loader, so we need to close only one time.
   *
   * @param jarName jar name to be removed
   */
  private void removeAllByJar(String jarName) {
    Map<String, Queue<String>> jar = jars.remove(jarName);
    if (jar == null) {
      return;
    }

    for (Map.Entry<String, Queue<String>> functionEntry : jar.entrySet()) {
      final String function = functionEntry.getKey();
      Map<String, DrillFuncHolder> functionHolders = functions.get(function);
      Queue<String> functionSignatures = functionEntry.getValue();
      for (Map.Entry<String, DrillFuncHolder> entry : functionHolders.entrySet()) {
        if (functionSignatures.contains(entry.getKey())) {
          ClassLoader classLoader = entry.getValue().getClassLoader();
          if (classLoader instanceof AutoCloseable) {
            try {
              ((AutoCloseable) classLoader).close();
            } catch (Exception e) {
              logger.warn("Problem during closing class loader", e);
            }
          }
          break;
        }
      }
      functionHolders.keySet().removeAll(functionSignatures);

      if (functionHolders.isEmpty()) {
        functions.remove(function);
      }
    }
  }
}
