/*
 * 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.streams.plugins;

import org.reflections.ReflectionUtils;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Embed within your own java code
 *
 * <p></p>
 * StreamsScalaGenerationConfig config = new StreamsScalaGenerationConfig();
 * config.setTargetDirectory("target/generated-sources/scala");
 * config.setTargetPackage("com.example");
 * StreamsScalaSourceGenerator generator = new StreamsScalaSourceGenerator(config);
 * generator.run();
 *
 */
public class StreamsScalaSourceGenerator implements Runnable {

  private static final Logger LOGGER = LoggerFactory.getLogger(StreamsScalaSourceGenerator.class);

  private static final String LS = System.getProperty("line.separator");

  private StreamsScalaGenerationConfig config;

  private Reflections reflections;

  private String outDir;

  /**
   * Run from CLI without Maven
   *
   * <p></p>
   * java -jar streams-plugin-scala-jar-with-dependencies.jar StreamsScalaSourceGenerator target/generated-sources
   *
   * @param args [targetDirectory, targetPackage]
   * */
  public static void main(String[] args) {
    StreamsScalaGenerationConfig config = new StreamsScalaGenerationConfig();

    List<String> sourcePackages = new ArrayList<>();
    String targetDirectory = "target/generated-sources/pojo";
    String targetPackage = "";

    if ( args.length > 0 ) {
      sourcePackages = Stream.of(args[0].split(",")).collect(Collectors.toList());
    }
    if ( args.length > 1 ) {
      targetDirectory = args[1];
    }
    if ( args.length > 2 ) {
      targetPackage = args[2];
    }

    config.setSourcePackages(sourcePackages);
    config.setTargetPackage(targetPackage);
    config.setTargetDirectory(targetDirectory);

    StreamsScalaSourceGenerator streamsScalaSourceGenerator = new StreamsScalaSourceGenerator(config);
    streamsScalaSourceGenerator.run();
  }

  /**
   * StreamsScalaSourceGenerator constructor.
   * @param config StreamsScalaGenerationConfig
   */
  public StreamsScalaSourceGenerator(StreamsScalaGenerationConfig config) {
    this.config = config;
    this.outDir = config.getTargetDirectory().getAbsolutePath();
    reflections = new Reflections(
        new ConfigurationBuilder()
            // TODO
            .forPackages(
                config.getSourcePackages()
                    .toArray(new String[config.getSourcePackages().size()])
            )
            .setScanners(
                new SubTypesScanner(),
                new TypeAnnotationsScanner()));

  }

  @Override
  public void run() {

    List<Class<?>> serializableClasses = detectSerializableClasses();

    LOGGER.info("Detected {} serialiables:", serializableClasses.size());
    for ( Class clazz : serializableClasses ) {
      LOGGER.debug(clazz.toString());
    }

    List<Class<?>> pojoClasses = detectPojoClasses(serializableClasses);

    LOGGER.info("Detected {} pojos:", pojoClasses.size());
    for ( Class clazz : pojoClasses ) {
      LOGGER.debug(clazz.toString());
    }

    List<Class<?>> traits = detectTraits(pojoClasses);

    LOGGER.info("Detected {} traits:", traits.size());
    for ( Class clazz : traits ) {
      LOGGER.debug(clazz.toString());
    }

    List<Class<?>> cases = detectCases(pojoClasses);

    LOGGER.info("Detected {} cases:", cases.size());
    for ( Class clazz : cases ) {
      LOGGER.debug(clazz.toString());
    }

    for ( Class clazz : traits ) {
      String pojoPath = clazz.getPackage().getName().replace(".pojo.json", ".scala").replace(".","/") + "/traits/";
      String pojoName = clazz.getSimpleName() + ".scala";
      String pojoScala = renderTrait(clazz);
      writeFile(outDir + "/" + pojoPath + pojoName, pojoScala);
    }

    for ( Class clazz : traits ) {
      String pojoPath = clazz.getPackage().getName().replace(".pojo.json", ".scala").replace(".","/") + "/";
      String pojoName = clazz.getSimpleName() + ".scala";
      String pojoScala = renderClass(clazz);
      writeFile(outDir + "/" + pojoPath + pojoName, pojoScala);
    }

    for ( Class clazz : cases ) {
      String pojoPath = clazz.getPackage().getName().replace(".pojo.json", ".scala").replace(".","/") + "/";
      String pojoName = clazz.getSimpleName() + ".scala";
      String pojoScala = renderCase(clazz);
      writeFile(outDir + "/" + pojoPath + pojoName, pojoScala);
    }

  }

  private void writeFile(String pojoFile, String pojoScala) {
    try {
      File path = new File(pojoFile);
      File dir = path.getParentFile();
      if ( !dir.exists() ) {
        dir.mkdirs();
      }
      Files.write(Paths.get(pojoFile), pojoScala.getBytes(), StandardOpenOption.CREATE_NEW);
    } catch (Exception ex) {
      LOGGER.error("Write Exception: {}", ex);
    }
  }

  /**
   * detectSerializableClasses.
   * @return List of Serializable Classes
   */
  public List<Class<?>> detectSerializableClasses() {

    Set<Class<? extends Serializable>> classes =
        reflections.getSubTypesOf(java.io.Serializable.class);

    List<Class<?>> result = new ArrayList<>();

    for ( Class clazz : classes ) {
      result.add(clazz);
    }

    return result;
  }

  /**
   * detect which Classes are Pojo Classes.
   * @param classes List of candidate Pojo Classes
   * @return List of actual Pojo Classes
   */
  public List<Class<?>> detectPojoClasses(List<Class<?>> classes) {

    List<Class<?>> result = new ArrayList<>();

    for ( Class clazz : classes ) {
      try {
        clazz.newInstance().toString();
      } catch ( Exception ex) {
        //
      }
      // super-halfass way to know if this is a jsonschema2pojo
      if ( clazz.getAnnotations().length >= 1 ) {
        result.add(clazz);
      }
    }

    return result;
  }

  private List<Class<?>> detectTraits(List<Class<?>> classes) {

    List<Class<?>> traits = new ArrayList<>();

    for ( Class clazz : classes ) {
      if (reflections.getSubTypesOf(clazz).size() > 0) {
        traits.add(clazz);
      }
    }

    return traits;
  }

  private List<Class<?>> detectCases(List<Class<?>> classes) {

    List<Class<?>> cases = new ArrayList<>();

    for ( Class clazz : classes ) {
      if (reflections.getSubTypesOf(clazz).size() == 0) {
        cases.add(clazz);
      }
    }

    return cases;
  }

  private String renderTrait(Class<?> pojoClass) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("package ");
    stringBuffer.append(pojoClass.getPackage().getName().replace(".pojo.json", ".scala"));
    stringBuffer.append(".traits");
    stringBuffer.append(LS);
    stringBuffer.append("trait ").append(pojoClass.getSimpleName());
    stringBuffer.append(" extends Serializable");
    stringBuffer.append(" {");

    Set<Field> fields = ReflectionUtils.getAllFields(pojoClass);
    appendFields(stringBuffer, fields, "def", ";");

    stringBuffer.append("}");

    return stringBuffer.toString();
  }

  private String renderClass(Class<?> pojoClass) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("package ");
    stringBuffer.append(pojoClass.getPackage().getName().replace(".pojo.json", ".scala"));
    stringBuffer.append(LS);
    stringBuffer.append("import org.apache.commons.lang3.builder.{HashCodeBuilder, EqualsBuilder, ToStringBuilder}");
    stringBuffer.append(LS);
    stringBuffer.append("class ").append(pojoClass.getSimpleName());
    stringBuffer.append(" (");

    Set<Field> fields = ReflectionUtils.getAllFields(pojoClass);
    appendFields(stringBuffer, fields, "var", ",");

    stringBuffer.append(")");
    stringBuffer.append(" extends ").append(pojoClass.getPackage().getName().replace(".pojo.json", ".scala")).append(".traits.").append(pojoClass.getSimpleName());
    stringBuffer.append(" with Serializable ");
    stringBuffer.append("{ ");
    stringBuffer.append(LS);
    stringBuffer.append("override def equals(obj: Any) = obj match { ");
    stringBuffer.append(LS);
    stringBuffer.append("  case other: ");
    stringBuffer.append(pojoClass.getSimpleName());
    stringBuffer.append(" => other.getClass == getClass && EqualsBuilder.reflectionEquals(this,obj)");
    stringBuffer.append(LS);
    stringBuffer.append("  case _ => false");
    stringBuffer.append(LS);
    stringBuffer.append("}");
    stringBuffer.append(LS);
    stringBuffer.append("override def hashCode = new HashCodeBuilder().hashCode");
    stringBuffer.append(LS);
    stringBuffer.append("}");

    return stringBuffer.toString();
  }

  private String renderCase(Class<?> pojoClass) {
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("package ");
    stringBuffer.append(pojoClass.getPackage().getName().replace(".pojo.json", ".scala"));
    stringBuffer.append(LS);
    stringBuffer.append("case class " + pojoClass.getSimpleName());
    stringBuffer.append("(");
    Set<Field> fields = ReflectionUtils.getAllFields(pojoClass);
    appendFields(stringBuffer, fields, "var", ",");
    stringBuffer.append(")");
    if ( pojoClass.getSuperclass() != null && !pojoClass.getSuperclass().equals(java.lang.Object.class)) {
      stringBuffer.append(" extends " + pojoClass.getSuperclass().getPackage().getName().replace(".pojo.json", ".scala") + ".traits." + pojoClass.getSuperclass().getSimpleName());
    }
    stringBuffer.append(LS);

    return stringBuffer.toString();
  }

  private void appendFields(StringBuffer stringBuffer, Set<Field> fields, String varDef, String fieldDelimiter) {
    if ( fields.size() > 0 ) {
      stringBuffer.append(LS);
      Map<String,Field> fieldsToAppend = uniqueFields(fields);
      for ( Iterator<Field> iter = fieldsToAppend.values().iterator(); iter.hasNext(); ) {
        Field field = iter.next();
        if ( override( field ) ) {
          stringBuffer.append("override ");
        }
        stringBuffer.append(varDef);
        stringBuffer.append(" ");
        stringBuffer.append(name(field));
        stringBuffer.append(": ");
        if ( option(field) ) {
          stringBuffer.append("scala.Option[");
          stringBuffer.append(type(field));
          stringBuffer.append("]");
        } else {
          stringBuffer.append(type(field));
        }
        if ( !fieldDelimiter.equals(";") && value(field) != null) {
          stringBuffer.append(" = ");
          if ( option(field) ) {
            stringBuffer.append("scala.Some(");
            stringBuffer.append(value(field));
            stringBuffer.append(")");
          } else {
            stringBuffer.append(value(field));
          }
        }
        if ( iter.hasNext()) {
          stringBuffer.append(fieldDelimiter);
        }
        stringBuffer.append(LS);
      }
    } else {
      stringBuffer.append(LS);
    }
  }

  private boolean option(Field field) {
    return !field.getName().equals("verb") &&
        !field.getType().equals(Map.class) &&
        !field.getType().equals(List.class);
  }

  private String value(Field field) {
    switch (field.getName()) {
      case "verb":
        return "\"post\"";
      case "objectType":
        return "\"application\"";
      default:
        return null;
    }
  }

  private String type(Field field) {
    if ( field.getType().equals(java.lang.String.class)) {
      return "String";
    } else if ( field.getType().equals(java.util.Map.class)) {
      return "scala.collection.mutable.Map[String,Any]";
    } else if ( field.getType().equals(java.util.List.class)) {
      return "scala.collection.mutable.MutableList[Any]";
    }
    return field.getType().getCanonicalName().replace(".pojo.json", ".scala");
  }

  private Map<String,Field> uniqueFields(Set<Field> fieldset) {
    Map<String,Field> fields = new TreeMap<>();
    Field item = null;
    for ( Iterator<Field> it = fieldset.iterator(); it.hasNext(); item = it.next() ) {
      if ( item != null && item.getName() != null ) {
        Field added = fields.put(item.getName(), item);
      }
      // ensure right class will get used
    }
    return fields;
  }

  private String name(Field field) {
    if ( field.getName().equals("object")) {
      return "obj";
    } else {
      return field.getName();
    }
  }

  private boolean override(Field field) {
    try {
      return field.getDeclaringClass().getSuperclass().getField(field.getName()) != null;
    } catch ( Exception ex ) {
      return false;
    }
  }
}
