/**
* 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.hadoop.yarn.webapp.hamlet;

import com.google.common.collect.Sets;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.regex.Pattern;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.util.StringUtils;
import org.apache.hadoop.yarn.webapp.WebAppException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Generates a specific hamlet implementation class from a spec class
 * using a generic hamlet implementation class.
 */
@InterfaceAudience.LimitedPrivate({"YARN", "MapReduce"})
public class HamletGen {
  static final Logger LOG = LoggerFactory.getLogger(HamletGen.class);
  static final Options opts = new Options();
  static {
    opts.addOption("h", "help", false, "Print this help message").
         addOption("s", "spec-class", true,
                   "The class that holds the spec interfaces. e.g. HamletSpec").
         addOption("i", "impl-class", true,
                   "An implementation class. e.g. HamletImpl").
         addOption("o", "output-class", true, "Output class name").
         addOption("p", "output-package", true, "Output package name");
  };

  static final Pattern elementRegex = Pattern.compile("^[A-Z][A-Z0-9]*$");

  int bytes = 0;
  PrintWriter out;
  final Set<String> endTagOptional = Sets.newHashSet();
  final Set<String> inlineElements = Sets.newHashSet();
  Class<?> top; // html top-level interface
  String hamlet; // output class simple name;
  boolean topMode;

  /**
   * Generate a specific Hamlet implementation from a spec.
   * @param specClass holds hamlet interfaces. e.g. {@link HamletSpec}
   * @param implClass a generic hamlet implementation. e.g. {@link HamletImpl}
   * @param outputName name of the output class. e.g. {@link Hamlet}
   * @param outputPkg package name of the output class.
   * @throws IOException
   */
  public void generate(Class<?> specClass, Class<?> implClass,
                       String outputName, String outputPkg) throws IOException {
    LOG.info("Generating {} using {} and {}", new Object[]{outputName,
             specClass, implClass});
    out = new PrintWriter(outputName +".java", "UTF-8");
    hamlet = basename(outputName);
    String pkg = pkgName(outputPkg, implClass.getPackage().getName());
    puts(0, "// Generated by HamletGen. Do NOT edit!\n",
         "package ", pkg, ";\n",
         "import java.io.PrintWriter;\n",
         "import java.util.EnumSet;\n",
         "import static java.util.EnumSet.*;\n",
         "import static ", implClass.getName(), ".EOpt.*;\n",
         "import org.apache.hadoop.yarn.webapp.SubView;");
    String implClassName = implClass.getSimpleName();
    if (!implClass.getPackage().getName().equals(pkg)) {
      puts(0, "import ", implClass.getName(), ';');
    }
    puts(0, "\n",
         "public class ", hamlet, " extends ", implClassName,
         " implements ", specClass.getSimpleName(), "._Html {\n",
         "  public ", hamlet, "(PrintWriter out, int nestLevel,",
         " boolean wasInline) {\n",
         "    super(out, nestLevel, wasInline);\n",
         "  }\n\n", // inline is context sensitive
         "  static EnumSet<EOpt> opt(boolean endTag, boolean inline, ",
         "boolean pre) {\n",
         "    EnumSet<EOpt> opts = of(ENDTAG);\n",
         "    if (!endTag) opts.remove(ENDTAG);\n",
         "    if (inline) opts.add(INLINE);\n",
         "    if (pre) opts.add(PRE);\n",
         "    return opts;\n",
         "  }");
    initLut(specClass);
    genImpl(specClass, implClassName, 1);
    LOG.info("Generating {} methods", hamlet);
    genMethods(hamlet, top, 1);
    puts(0, "}");
    out.close();
    LOG.info("Wrote {} bytes to {}.java", bytes, outputName);
  }

  String basename(String path) {
    return path.substring(path.lastIndexOf('/') + 1);
  }

  String pkgName(String pkg, String defaultPkg) {
    if (pkg == null || pkg.isEmpty()) return defaultPkg;
    return pkg;
  }

  void initLut(Class<?> spec) {
    endTagOptional.clear();
    inlineElements.clear();
    for (Class<?> cls : spec.getClasses()) {
      Annotation a = cls.getAnnotation(HamletSpec.Element.class);
      if (a != null && !((HamletSpec.Element) a).endTag()) {
        endTagOptional.add(cls.getSimpleName());
      }
      if (cls.getSimpleName().equals("Inline")) {
        for (Method method : cls.getMethods()) {
          String retName = method.getReturnType().getSimpleName();
          if (isElement(retName)) {
            inlineElements.add(retName);
          }
        }
      }
    }
  }

  void genImpl(Class<?> spec, String implClassName, int indent) {
    String specName = spec.getSimpleName();
    for (Class<?> cls : spec.getClasses()) {
      String className = cls.getSimpleName();
      if (cls.isInterface()) {
        genFactoryMethods(cls, indent);
      }
      if (isElement(className)) {
        LOG.info("Generating class {}<T>", className);
        puts(indent, "\n",
             "public class ", className, "<T extends _>",
             " extends EImp<T> implements ", specName, ".", className, " {\n",
             "  public ", className, "(String name, T parent,",
             " EnumSet<EOpt> opts) {\n",
             "    super(name, parent, opts);\n",
             "  }");
        genMethods(className, cls, indent + 1);
        puts(indent, "}");
      } else if (className.equals("_Html")) {
        top = cls;
      }
    }
  }

  void genFactoryMethods(Class<?> cls, int indent) {
    for (Method method : cls.getDeclaredMethods()) {
      String retName = method.getReturnType().getSimpleName();
      String methodName = method.getName();
      if (methodName.charAt(0) == '$') continue;
      if (isElement(retName) && method.getParameterTypes().length == 0) {
        genFactoryMethod(retName, methodName, indent);
      }
    }
  }

  void genMethods(String className, Class<?> cls, int indent) {
    topMode = (top != null && cls.equals(top));
    for (Method method : cls.getMethods()) {
      String retName = method.getReturnType().getSimpleName();
      if (method.getName().charAt(0) == '$') {
        genAttributeMethod(className, method, indent);
      } else if (isElement(retName)) {
        genNewElementMethod(className, method, indent);
      } else {
        genCurElementMethod(className, method, indent);
      }
    }
  }

  void genAttributeMethod(String className, Method method, int indent) {
    String methodName = method.getName();
    String attrName = methodName.substring(1).replace('_', '-');
    Type[] params = method.getGenericParameterTypes();
    echo(indent, "\n",
         "@Override\n",
         "public ", className, topMode ? " " : "<T> ", methodName, "(");
    if (params.length == 0) {
      puts(0, ") {");
      puts(indent,
           "  addAttr(\"", attrName, "\", null);\n",
           "  return this;\n", "}");
    } else if (params.length == 1) {
      String typeName = getTypeName(params[0]);
      puts(0, typeName, " value) {");
      if (typeName.equals("EnumSet<LinkType>")) {
        puts(indent,
             "  addRelAttr(\"", attrName, "\", value);\n",
             "  return this;\n", "}");
      } else if (typeName.equals("EnumSet<Media>")) {
        puts(indent,
             "  addMediaAttr(\"", attrName, "\", value);\n",
             "  return this;\n", "}");
      } else {
        puts(indent,
             "  addAttr(\"", attrName, "\", value);\n",
             "  return this;\n", "}");
      }
    } else {
      throwUnhandled(className, method);
    }
  }

  String getTypeName(Type type) {
    if (type instanceof Class<?>) {
      return ((Class<?>)type).getSimpleName();
    }
    ParameterizedType pt = (ParameterizedType) type;
    return ((Class<?>)pt.getRawType()).getSimpleName() +"<"+
        ((Class<?>)pt.getActualTypeArguments()[0]).getSimpleName() +">";
  }

  void genFactoryMethod(String retName, String methodName, int indent) {
    puts(indent, "\n",
         "private <T extends _> ", retName, "<T> ", methodName,
         "_(T e, boolean inline) {\n",
         "  return new ", retName, "<T>(\"", StringUtils.toLowerCase(retName),
         "\", e, opt(", !endTagOptional.contains(retName), ", inline, ",
         retName.equals("PRE"), ")); }");
  }

  void genNewElementMethod(String className, Method method, int indent) {
    String methodName = method.getName();
    String retName = method.getReturnType().getSimpleName();
    Class<?>[] params = method.getParameterTypes();
    echo(indent, "\n",
         "@Override\n",
         "public ", retName, "<", className, topMode ? "> " : "<T>> ",
         methodName, "(");
    if (params.length == 0) {
      puts(0, ") {");
      puts(indent,
           topMode ? "" : "  closeAttrs();\n",
           "  return ", StringUtils.toLowerCase(retName), "_" + "(this, ",
           isInline(className, retName), ");\n", "}");
    } else if (params.length == 1) {
      puts(0, "String selector) {");
      puts(indent,
           "  return setSelector(", methodName, "(), selector);\n", "}");
    } else {
      throwUnhandled(className, method);
    }
  }

  boolean isInline(String container, String className) {
    if ((container.equals("BODY") || container.equals(hamlet) ||
         container.equals("HEAD") || container.equals("HTML")) &&
        (className.equals("INS") || className.equals("DEL") ||
         className.equals("SCRIPT"))) {
      return false;
    }
    return inlineElements.contains(className);
  }

  void genCurElementMethod(String className, Method method, int indent) {
    String methodName = method.getName();
    Class<?>[] params = method.getParameterTypes();
    if (topMode || params.length > 0) {
      echo(indent, "\n",
         "@Override\n",
         "public ", className, topMode ? " " : "<T> ", methodName, "(");
    }
    if (params.length == 0) {
      if (topMode) {
        puts(0, ") {");
        puts(indent, "  return this;\n", "}");
      }
    } else if (params.length == 1) {
      if (methodName.equals("base")) {
        puts(0, "String href) {");
        puts(indent,
             "  return base().$href(href)._();\n", "}");
      } else if (methodName.equals("script")) {
        puts(0, "String src) {");
        puts(indent,
             "  return setScriptSrc(script(), src)._();\n", "}");
      } else if (methodName.equals("style")) {
        puts(0, "Object... lines) {");
        puts(indent,
             "  return style().$type(\"text/css\")._(lines)._();\n", "}");
      } else if (methodName.equals("img")) {
        puts(0, "String src) {");
        puts(indent,
             "  return ", methodName, "().$src(src)._();\n", "}");
      } else if (methodName.equals("br") || methodName.equals("hr") ||
                 methodName.equals("col")) {
        puts(0, "String selector) {");
        puts(indent,
             "  return setSelector(", methodName, "(), selector)._();\n", "}");
      }  else if (methodName.equals("link")) {
        puts(0, "String href) {");
        puts(indent,
             "  return setLinkHref(", methodName, "(), href)._();\n", "}");
      } else if (methodName.equals("_")) {
        if (params[0].getSimpleName().equals("Class")) {
          puts(0, "Class<? extends SubView> cls) {");
          puts(indent,
               "  ", topMode ? "subView" : "_v", "(cls);\n",
               "  return this;\n", "}");
        } else {
          puts(0, "Object... lines) {");
          puts(indent,
               "  _p(", needsEscaping(className), ", lines);\n",
               "  return this;\n", "}");
        }
      } else if (methodName.equals("_r")) {
        puts(0, "Object... lines) {");
        puts(indent,
             "  _p(false, lines);\n",
             "  return this;\n", "}");
      } else {
        puts(0, "String cdata) {");
        puts(indent,
             "  return ", methodName, "()._(cdata)._();\n", "}");
      }
    } else if (params.length == 2) {
      if (methodName.equals("meta")) {
        puts(0, "String name, String content) {");
        puts(indent,
             "  return meta().$name(name).$content(content)._();\n", "}");
      } else if (methodName.equals("meta_http")) {
        puts(0, "String header, String content) {");
        puts(indent,
             "  return meta().$http_equiv(header).$content(content)._();\n",
             "}");
      } else if (methodName.equals("a")) {
        puts(0, "String href, String anchorText) {");
        puts(indent,
             "  return a().$href(href)._(anchorText)._();\n", "}");
      } else if (methodName.equals("bdo")) {
        puts(0, "Dir dir, String cdata) {");
        puts(indent, "  return bdo().$dir(dir)._(cdata)._();\n", "}");
      } else if (methodName.equals("label")) {
        puts(0, "String forId, String cdata) {");
        puts(indent, "  return label().$for(forId)._(cdata)._();\n", "}");
      } else if (methodName.equals("param")) {
        puts(0, "String name, String value) {");
        puts(indent,
             "  return param().$name(name).$value(value)._();\n", "}");
      } else {
        puts(0, "String selector, String cdata) {");
        puts(indent,
             "  return setSelector(", methodName,
             "(), selector)._(cdata)._();\n", "}");
      } 
    } else if (params.length == 3) {
      if (methodName.equals("a")) {
        puts(0, "String selector, String href, String anchorText) {");
        puts(indent,
             "  return setSelector(a(), selector)",
             ".$href(href)._(anchorText)._();\n", "}");
      }
    } else {
      throwUnhandled(className, method);
    }
  }

  static boolean needsEscaping(String eleName) {
    return !eleName.equals("SCRIPT") && !eleName.equals("STYLE");
  }

  static void throwUnhandled(String className, Method method) {
    throw new WebAppException("Unhandled " + className + "#" + method);
  }

  void echo(int indent, Object... args) {
    String prev = null;
    for (Object o : args) {
      String s = String.valueOf(o);
      if (!s.isEmpty() && !s.equals("\n") &&
          (prev == null || prev.endsWith("\n"))) {
        indent(indent);
      }
      prev = s;
      out.print(s);
      bytes += s.length();
    }
  }

  void indent(int indent) {
    for (int i = 0; i < indent; ++i) {
      out.print("  ");
      bytes += 2;
    }
  }

  void puts(int indent, Object... args) {
    echo(indent, args);
    out.println();
    ++bytes;
  }

  boolean isElement(String s) {
    return elementRegex.matcher(s).matches();
  }

  public static void main(String[] args) throws Exception {
    CommandLine cmd = new GnuParser().parse(opts, args);
    if (cmd.hasOption("help")) {
      new HelpFormatter().printHelp("Usage: hbgen [OPTIONS]", opts);
      return;
    }
    // defaults
    Class<?> specClass = HamletSpec.class;
    Class<?> implClass = HamletImpl.class;
    String outputClass = "HamletTmp";
    String outputPackage = implClass.getPackage().getName();
    if (cmd.hasOption("spec-class")) {
      specClass = Class.forName(cmd.getOptionValue("spec-class"));
    }
    if (cmd.hasOption("impl-class")) {
      implClass = Class.forName(cmd.getOptionValue("impl-class"));
    }
    if (cmd.hasOption("output-class")) {
      outputClass = cmd.getOptionValue("output-class");
    }
    if (cmd.hasOption("output-package")) {
      outputPackage = cmd.getOptionValue("output-package");
    }
    new HamletGen().generate(specClass, implClass, outputClass, outputPackage);
  }
}
