/*
 * Copyright 2008 Google Inc.
 *
 * 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 com.google.gwt.uibinder.rebind;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dev.util.Util;
import com.google.gwt.uibinder.client.UiTemplate;
import com.google.gwt.uibinder.rebind.messages.MessagesWriter;
import com.google.gwt.uibinder.rebind.model.ImplicitClientBundle;
import org.jboss.errai.ioc.client.api.PackageTarget;
import org.w3c.dom.Document;
import org.xml.sax.SAXParseException;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

/**
 * Generator for implementations of
 * {@link com.google.gwt.uibinder.client.UiBinder}.
 */
public class UiBinderGenerator extends Generator {

  private static final String BINDER_URI = "urn:ui:com.google.gwt.uibinder";

  private static final String TEMPLATE_SUFFIX = ".ui.xml";

  private static final String XSS_SAFE_CONFIG_PROPERTY = "UiBinder.useSafeHtmlTemplates";
  private static final String LAZY_WIDGET_BUILDERS_PROPERTY = "UiBinder.useLazyWidgetBuilders";

  private static boolean gaveSafeHtmlWarning;
  private static boolean gaveLazyBuildersWarning;

  /**
   * Given a UiBinder interface, return the path to its ui.xml file, suitable
   * for any classloader to find it as a resource.
   */
  private static String deduceTemplateFile(MortalLogger logger,
                                           JClassType interfaceType) throws UnableToCompleteException {
    String templateName = null;
    UiTemplate annotation = interfaceType.getAnnotation(UiTemplate.class);
    if (annotation == null) {
      // if the interface is defined as a nested class, use the name of the
      // enclosing type
      if (interfaceType.getEnclosingType() != null) {
        interfaceType = interfaceType.getEnclosingType();
      }
      return slashify(interfaceType.getQualifiedBinaryName()) + TEMPLATE_SUFFIX;
    }
    else {
      templateName = annotation.value();
      if (!templateName.endsWith(TEMPLATE_SUFFIX)) {
        logger.die("Template file name must end with " + TEMPLATE_SUFFIX);
      }

      /*
       * If the template file name (minus suffix) has no dots, make it relative
       * to the binder's package, otherwise slashify the dots
       */
      String unsuffixed = templateName.substring(0,
              templateName.lastIndexOf(TEMPLATE_SUFFIX));
      if (!unsuffixed.contains(".")) {
        templateName = slashify(determinePackage(interfaceType)) + "/"
                + templateName;
      }
      else {
        templateName = slashify(unsuffixed) + TEMPLATE_SUFFIX;
      }
    }
    return templateName;
  }

  private static String slashify(String s) {
    return s.replace(".", "/").replace("$", ".");
  }

  private final UiBinderContext uiBinderCtx = new UiBinderContext();

  @Override
  public String generate(TreeLogger logger, GeneratorContext genCtx,
                         String fqInterfaceName) throws UnableToCompleteException {
    TypeOracle oracle = genCtx.getTypeOracle();
    ResourceOracle resourceOracle = genCtx.getResourcesOracle();

    JClassType interfaceType;
    try {
      interfaceType = oracle.getType(fqInterfaceName);
    }
    catch (NotFoundException e) {
      throw new RuntimeException(e);
    }

    DesignTimeUtils designTime;
    if (DesignTimeUtilsImpl.isDesignTime(fqInterfaceName)) {
      designTime = new DesignTimeUtilsImpl();
    }
    else {
      designTime = DesignTimeUtilsStub.EMPTY;
    }

    String implName = interfaceType.getName().replace('.', '_') + "Impl";
    implName = designTime.getImplName(implName);

    String packageName = determinePackage(interfaceType);

    PrintWriterManager writers = new PrintWriterManager(genCtx, logger,
            packageName);
    PrintWriter printWriter = writers.tryToMakePrintWriterFor(implName);

    if (printWriter != null) {
      generateOnce(interfaceType, implName, printWriter, logger, oracle,
              resourceOracle, genCtx.getPropertyOracle(), writers, designTime);
    }
    return packageName + "." + implName;
  }

  private Boolean extractConfigProperty(MortalLogger logger,
                                        PropertyOracle propertyOracle, String configProperty, boolean defaultValue) {
    List<String> values;
    try {
      values = propertyOracle.getConfigurationProperty(configProperty).getValues();
    }
    catch (BadPropertyValueException e) {
      logger.warn("No value found for configuration property %s.", configProperty);
      return defaultValue;
    }

    String value = values.get(0);
    if (!value.equals(Boolean.FALSE.toString()) && !value.equals(Boolean.TRUE.toString())) {
      logger.warn("Unparseable value \"%s\" found for configuration property %s", value,
              configProperty);
      return defaultValue;
    }

    return Boolean.valueOf(value);
  }

  private static String determinePackage(JClassType interfaceType) {
    String packageName;

    if (interfaceType.isAnnotationPresent(PackageTarget.class)) {
      packageName = interfaceType.getAnnotation(PackageTarget.class).value();
    }
    else {
      packageName = interfaceType.getPackage().getName();
    }

    return packageName;
  }

  private void generateOnce(JClassType interfaceType, String implName,
                            PrintWriter binderPrintWriter, TreeLogger treeLogger, TypeOracle oracle,
                            ResourceOracle resourceOracle, PropertyOracle propertyOracle,
                            PrintWriterManager writerManager, DesignTimeUtils designTime)
          throws UnableToCompleteException {

    MortalLogger logger = new MortalLogger(treeLogger);
    String templatePath = deduceTemplateFile(logger, interfaceType);

    MessagesWriter messages = new MessagesWriter(oracle, BINDER_URI, logger,
            templatePath, determinePackage(interfaceType), implName);

    boolean useLazyWidgetBuilders = useLazyWidgetBuilders(logger, propertyOracle) && !designTime.isDesignTime();
    FieldManager fieldManager = new FieldManager(oracle, logger, useLazyWidgetBuilders);

    UiBinderWriter uiBinderWriter = new UiBinderWriter(interfaceType, implName,
            templatePath, oracle, logger, fieldManager, messages, designTime, uiBinderCtx,
            useSafeHtmlTemplates(logger, propertyOracle), useLazyWidgetBuilders, BINDER_URI);

    Document doc = getW3cDoc(logger, designTime, resourceOracle, templatePath);
    designTime.rememberPathForElements(doc);

    uiBinderWriter.parseDocument(doc, binderPrintWriter);

    if (messages.hasMessages()) {
      messages.write(writerManager.makePrintWriterFor(messages.getMessagesClassName()));
    }

    ImplicitClientBundle bundleClass = uiBinderWriter.getBundleClass();
    new BundleWriter(bundleClass, writerManager, oracle, logger).write();

    writerManager.commit();
  }

  private Document getW3cDoc(MortalLogger logger, DesignTimeUtils designTime,
                             ResourceOracle resourceOracle, String templatePath)
          throws UnableToCompleteException {

    Resource resource = resourceOracle.getResourceMap().get(templatePath);
    if (null == resource) {
      logger.die("Unable to find resource: " + templatePath);
    }

    Document doc = null;
    try {
      String content = designTime.getTemplateContent(templatePath);
      if (content == null) {
        content = Util.readStreamAsString(resource.openContents());
      }
      doc = new W3cDomHelper(logger.getTreeLogger(), resourceOracle).documentFor(
              content, resource.getPath());
    } catch (IOException iex) {
      logger.die("Error opening resource:" + resource.getLocation(), iex);
	} catch (SAXParseException e) {
	  logger.die(
	     "Error parsing XML (line " + e.getLineNumber() + "): "
	         + e.getMessage(), e);
	}
    return doc;
  }

  private Boolean useLazyWidgetBuilders(MortalLogger logger, PropertyOracle propertyOracle) {
    Boolean rtn = extractConfigProperty(logger, propertyOracle, LAZY_WIDGET_BUILDERS_PROPERTY, true);
    if (!gaveLazyBuildersWarning && !rtn) {
      logger.warn("Configuration property %s is false. Deprecated code generation is in play. " +
              "This property will soon become a no-op.",
              LAZY_WIDGET_BUILDERS_PROPERTY);
      gaveLazyBuildersWarning = true;
    }
    return rtn;
  }

  private Boolean useSafeHtmlTemplates(MortalLogger logger, PropertyOracle propertyOracle) {
    Boolean rtn = extractConfigProperty(
            logger, propertyOracle, XSS_SAFE_CONFIG_PROPERTY, true);

    if (!gaveSafeHtmlWarning && !rtn) {
      logger.warn("Configuration property %s is false! UiBinder SafeHtml integration is off, "
              + "leaving your users more vulnerable to cross-site scripting attacks. This property " +
              "will soon become a no-op, and SafeHtml integration will always be on.",
              XSS_SAFE_CONFIG_PROPERTY);
      gaveSafeHtmlWarning = true;
    }
    return rtn;
  }
}
