/*
 * 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.zeppelin.helium;

import org.apache.zeppelin.dep.DependencyResolver;
import org.apache.zeppelin.resource.DistributedResourcePool;
import org.apache.zeppelin.resource.Resource;
import org.apache.zeppelin.resource.ResourcePool;
import org.apache.zeppelin.resource.ResourceSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Load application
 */
public class ApplicationLoader {
  private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationLoader.class);

  private final DependencyResolver depResolver;
  private final ResourcePool resourcePool;
  private final Map<HeliumPackage, Class<Application>> cached;

  public ApplicationLoader(ResourcePool resourcePool, DependencyResolver depResolver) {
    this.depResolver = depResolver;
    this.resourcePool = resourcePool;
    cached = Collections.synchronizedMap(
        new HashMap<HeliumPackage, Class<Application>>());
  }

  /**
   * Information of loaded application
   */
  private static class RunningApplication {
    HeliumPackage packageInfo;
    String noteId;
    String paragraphId;

    RunningApplication(HeliumPackage packageInfo, String noteId, String paragraphId) {
      this.packageInfo = packageInfo;
      this.noteId = noteId;
      this.paragraphId = paragraphId;
    }

    public HeliumPackage getPackageInfo() {
      return packageInfo;
    }

    public String getNoteId() {
      return noteId;
    }

    public String getParagraphId() {
      return paragraphId;
    }

    @Override
    public int hashCode() {
      return (paragraphId + noteId + packageInfo.getArtifact() + packageInfo.getClassName())
          .hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof RunningApplication)) {
        return false;
      }

      RunningApplication r = (RunningApplication) o;
      return packageInfo.equals(r.getPackageInfo()) && paragraphId.equals(r.getParagraphId()) &&
          noteId.equals(r.getNoteId());
    }
  }

  /**
   *
   * Instantiate application
   *
   * @param packageInfo
   * @param context
   * @return
   * @throws Exception
   */
  public Application load(HeliumPackage packageInfo, ApplicationContext context)
      throws Exception {
    if (packageInfo.getType() != HeliumType.APPLICATION) {
      throw new ApplicationException(
          "Can't instantiate " + packageInfo.getType() + " package using ApplicationLoader");
    }

    // check if already loaded
    RunningApplication key =
        new RunningApplication(packageInfo, context.getNoteId(), context.getParagraphId());

    // get resource required by this package
    ResourceSet resources = findRequiredResourceSet(packageInfo.getResources(),
        context.getNoteId(), context.getParagraphId());

    // load class
    Class<Application> appClass = loadClass(packageInfo);

    // instantiate
    ClassLoader oldcl = Thread.currentThread().getContextClassLoader();
    ClassLoader cl = appClass.getClassLoader();
    Thread.currentThread().setContextClassLoader(cl);
    try {
      Constructor<Application> constructor = appClass.getConstructor(ApplicationContext.class);

      Application app = new ClassLoaderApplication(constructor.newInstance(context), cl);
      return app;
    } catch (Exception e) {
      throw new ApplicationException(e);
    } finally {
      Thread.currentThread().setContextClassLoader(oldcl);
    }
  }

  public ResourceSet findRequiredResourceSet(
      String [][] requiredResources, String noteId, String paragraphId) {
    if (requiredResources == null || requiredResources.length == 0) {
      return new ResourceSet();
    }

    ResourceSet allResources;
    if (resourcePool instanceof DistributedResourcePool) {
      allResources = ((DistributedResourcePool) resourcePool).getAll(false);
    } else {
      allResources = resourcePool.getAll();
    }

    return findRequiredResourceSet(requiredResources, noteId, paragraphId, allResources);
  }

  static ResourceSet findRequiredResourceSet(String [][] requiredResources,
                                             String noteId,
                                             String paragraphId,
                                             ResourceSet resources) {
    ResourceSet args = new ResourceSet();
    if (requiredResources == null || requiredResources.length == 0) {
      return args;
    }

    resources = resources.filterByNoteId(noteId).filterByParagraphId(paragraphId);

    for (String [] requires : requiredResources) {
      args.clear();

      for (String require : requires) {
        boolean found = false;

        for (Resource r : resources) {
          if (require.startsWith(":") && r.getClassName().equals(require.substring(1))) {
            found = true;
          } else if (r.getResourceId().getName().equals(require)) {
            found = true;
          }

          if (found) {
            args.add(r);
            break;
          }
        }

        if (found == false) {
          break;
        }
      }

      if (args.size() == requires.length) {
        return args;
      }
    }

    return null;
  }


  private Class<Application> loadClass(HeliumPackage packageInfo) throws Exception {
    if (cached.containsKey(packageInfo)) {
      return cached.get(packageInfo);
    }

    // Create Application classloader
    List<URL> urlList = new LinkedList<>();

    // load artifact
    if (packageInfo.getArtifact() != null) {
      List<File> paths = depResolver.load(packageInfo.getArtifact());

      if (paths != null) {

        for (File path : paths) {
          urlList.add(path.toURI().toURL());
        }
      }
    }
    URLClassLoader applicationClassLoader =
        new URLClassLoader(
            urlList.toArray(new URL[]{}),
            Thread.currentThread().getContextClassLoader());

    Class<Application> cls =
        (Class<Application>) applicationClassLoader.loadClass(packageInfo.getClassName());
    cached.put(packageInfo, cls);
    return cls;
  }
}
