// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.
package org.eclipse.che.dto.generator;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.che.dto.server.DtoFactoryVisitor;
import org.eclipse.che.dto.shared.DTO;
import org.eclipse.che.dto.shared.DTOImpl;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

/**
 * Simple source generator that takes in the packages list with interface definitions and generates
 * client and server DTO impls.
 */
public class DtoGenerator {
  /** Flag: location of the packages that contains dto interfaces. */
  private String[] dtoPackages = null;

  /** Flag: Name of the generated java class file that contains the DTOs. */
  private String genFileName = "DataObjects.java";

  /** Flag: The type of impls to be generated, either client or server. */
  private String impl = "client";

  /**
   * Flag: A pattern we can use to search an absolute path and find the start of the package
   * definition.")
   */
  private String packageBase = "java.";

  public static void main(String[] args) {
    DtoGenerator generator = new DtoGenerator();
    for (String arg : args) {
      if (arg.startsWith("--dto_packages=")) {
        generator.setDtoPackages(arg.substring("--dto_packages=".length()));
      } else if (arg.startsWith("--gen_file_name=")) {
        generator.setGenFileName(arg.substring("--gen_file_name=".length()));
      } else if (arg.startsWith("--impl=")) {
        generator.setImpl(arg.substring("--impl=".length()));
      } else if (arg.startsWith("--package_base=")) {
        generator.setPackageBase(arg.substring("--package_base=".length()));
      } else {
        throw new RuntimeException("Unknown flag: " + arg);
        // System.exit(1);
      }
    }
    generator.generate();
  }

  private static Set<URL> getClasspathForPackages(String[] packages) {
    Set<URL> urls = new HashSet<>();
    for (String pack : packages) {
      urls.addAll(ClasspathHelper.forPackage(pack));
    }
    return urls;
  }

  public String[] getDtoPackages() {
    return dtoPackages;
  }

  private void setDtoPackages(String packagesParam) {
    setDtoPackages(packagesParam.split(","));
  }

  public void setDtoPackages(String[] dtoPackages) {
    this.dtoPackages = new String[dtoPackages.length];
    System.arraycopy(dtoPackages, 0, this.dtoPackages, 0, this.dtoPackages.length);
  }

  public String getGenFileName() {
    return genFileName;
  }

  public void setGenFileName(String genFileName) {
    this.genFileName = genFileName;
  }

  public String getImpl() {
    return impl;
  }

  public void setImpl(String impl) {
    this.impl = impl;
  }

  public String getPackageBase() {
    return packageBase;
  }

  public void setPackageBase(String packageBase) {
    this.packageBase = packageBase;
  }

  public void generate() {

    Set<URL> urls = getClasspathForPackages(dtoPackages);
    genFileName = genFileName.replace('/', File.separatorChar);
    String outputFilePath = genFileName;

    // Extract the name of the output file that will contain all the DTOs and its package.
    String myPackageBase = packageBase;
    if (!myPackageBase.endsWith("/")) {
      myPackageBase += "/";
    }
    myPackageBase = myPackageBase.replace('/', File.separatorChar);

    int packageStart = outputFilePath.lastIndexOf(myPackageBase) + myPackageBase.length();
    int packageEnd = outputFilePath.lastIndexOf(File.separatorChar);
    String fileName = outputFilePath.substring(packageEnd + 1);
    String className = fileName.substring(0, fileName.indexOf(".java"));
    String packageName =
        outputFilePath.substring(packageStart, packageEnd).replace(File.separatorChar, '.');

    File outFile = new File(outputFilePath);

    try {
      DtoTemplate dtoTemplate = new DtoTemplate(packageName, className, impl);
      Reflections reflection =
          new Reflections(
              new ConfigurationBuilder()
                  .setUrls(urls)
                  .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner()));

      List<Class<?>> dtos = new ArrayList<>(reflection.getTypesAnnotatedWith(DTO.class));

      // We sort alphabetically to ensure deterministic order of routing types.
      Collections.sort(dtos, new ClassesComparator());

      for (Class<?> clazz : dtos) {

        // DTO are interface
        if (clazz.isInterface()) {
          dtoTemplate.addInterface(clazz);
        }
      }

      reflection =
          new Reflections(
              new ConfigurationBuilder()
                  .setUrls(ClasspathHelper.forClassLoader())
                  .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner()));
      List<Class<?>> dtosDependencies =
          new ArrayList<>(reflection.getTypesAnnotatedWith(DTO.class));
      dtosDependencies.removeAll(dtos);

      reflection =
          new Reflections(
              new ConfigurationBuilder()
                  .setUrls(ClasspathHelper.forClassLoader())
                  .setScanners(new SubTypesScanner()));

      for (Class<?> clazz : dtosDependencies) {
        for (Class impl : reflection.getSubTypesOf(clazz)) {
          if (!(impl.isInterface()
              || urls.contains(impl.getProtectionDomain().getCodeSource().getLocation()))) {
            if ("client".equals(dtoTemplate.getImplType())) {
              if (isClientImpl(impl)) {
                dtoTemplate.addImplementation(clazz, impl);
              }
            } else if ("server".equals(dtoTemplate.getImplType())) {
              if (isServerImpl(impl)) {
                dtoTemplate.addImplementation(clazz, impl);
              }
            }
          }
        }
      }

      // Emit the generated file.
      Files.createDirectories(outFile.toPath().getParent());
      try (BufferedWriter writer = new BufferedWriter(new FileWriter(outFile))) {
        writer.write(dtoTemplate.toString());
      }

      if ("server".equals(impl)) {
        // Create file in META-INF/services/
        File outServiceFile =
            new File(
                myPackageBase + "META-INF/services/" + DtoFactoryVisitor.class.getCanonicalName());
        Files.createDirectories(outServiceFile.toPath().getParent());
        try (BufferedWriter serviceFileWriter =
            new BufferedWriter(new FileWriter(outServiceFile))) {
          serviceFileWriter.write(packageName + "." + className);
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private boolean isClientImpl(Class<?> impl) {
    DTOImpl a = impl.getAnnotation(DTOImpl.class);
    return a != null && "client".equals(a.value());
  }

  private boolean isServerImpl(Class<?> impl) {
    DTOImpl a = impl.getAnnotation(DTOImpl.class);
    return a != null && "server".equals(a.value());
  }

  private static class ClassesComparator implements Comparator<Class> {
    @Override
    public int compare(Class o1, Class o2) {
      return o1.getName().compareTo(o2.getName());
    }
  }
}
