/*
 * Copyright 2012 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.validation.rebind;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.GeneratorContext;
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.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.validation.client.impl.GwtSpecificValidator;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.PropertyDescriptor;

/**
 * A cache and factory for BeanHelpers. There should be one BeanHelperCache per
 * compilation run.
 * <p>
 * (public for tests)
 */
public class BeanHelperCache { // public for testing

  private final Map<JClassType, BeanHelper> cache;
  private final Validator serverSideValidator;

  /**
   * Creates a cache. There should be one cache per compiler run.
   * (public for tests.)
   */
  public BeanHelperCache() {
    cache = new HashMap<JClassType, BeanHelper>();
    serverSideValidator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  /**
   * Clears the cache.
   * (Public for testing.)
   */
  public void clear() {
    cache.clear();
  }

  /**
   * Creates a BeanHelper and writes an interface containing its instance. Also,
   * recursively creates any BeanHelpers on its constrained properties.
   * (Public for testing.)
   */
  public BeanHelper createHelper(Class<?> clazz, TreeLogger logger,
      GeneratorContext context) throws UnableToCompleteException {
    JClassType beanType = context.getTypeOracle().findType(clazz.getCanonicalName());
    return doCreateHelper(clazz, beanType, logger, context);
  }

  /**
   * Creates a BeanHelper and writes an interface containing its instance. Also,
   * recursively creates any BeanHelpers on its constrained properties.
   */
  BeanHelper createHelper(JClassType jType, TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    JClassType erasedType = jType.getErasedType();
    try {
      Class<?> clazz = Class.forName(erasedType.getQualifiedBinaryName());
      return doCreateHelper(clazz, erasedType, logger, context);
    } catch (ClassNotFoundException e) {
      logger.log(TreeLogger.ERROR, "Unable to create BeanHelper for "
          + erasedType, e);
      throw new UnableToCompleteException();
    }
  }

  List<BeanHelper> getAllBeans() {
    return Util.sortMostSpecificFirst(cache.values(), BeanHelper.TO_CLAZZ);
  }

  BeanHelper getBean(JClassType key) {
    return cache.get(key);
  }

  boolean isClassConstrained(Class<?> clazz) {
    return serverSideValidator.getConstraintsForClass(clazz).isBeanConstrained();
  }

  private BeanHelper doCreateHelper(Class<?> clazz,
      JClassType beanType, TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    BeanHelper helper = getBean(beanType);
    if (helper == null) {
      BeanDescriptor bean;
      try {
        bean = serverSideValidator.getConstraintsForClass(clazz);
      } catch (ValidationException e) {
        logger.log(TreeLogger.ERROR,
            "Unable to create a validator for " + clazz.getCanonicalName()
                + " because " + e.getMessage(), e);
        throw new UnableToCompleteException();
      }
      helper = new BeanHelper(beanType, clazz, bean);
      cache.put(helper.getJClass(), helper);

      writeInterface(context, logger, helper);

      // now recurse on all Cascaded elements
      for (PropertyDescriptor p : bean.getConstrainedProperties()) {
        // TODO(idol) only bother creating objects for properties that have constrains in the groups
        // specified in @GwtValidation, but not others
        if (p.isCascaded()) {
          doCreateHelperForProp(p, helper, logger, context);
        }
      }
    }
    return helper;
  }

  /**
   * Creates the appropriate BeanHelper for a property on a bean.
   */
  private void doCreateHelperForProp(PropertyDescriptor p, BeanHelper parent,
      TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    Class<?> elementClass = p.getElementClass();
    if (GwtSpecificValidatorCreator.isIterableOrMap(elementClass)) {
      if (parent.hasField(p)) {
        JClassType type = parent.getAssociationType(p, true);

        createHelper(type.getErasedType(), logger, context);
      }
      if (parent.hasGetter(p)) {
        JClassType type = parent.getAssociationType(p, false);

        createHelper(type.getErasedType(), logger, context);
      }
    } else {
      if (serverSideValidator.getConstraintsForClass(elementClass).isBeanConstrained()) {
        createHelper(elementClass, logger, context);
      }
    }
  }

  /**
   * Write an Empty Interface implementing
   * {@link com.google.gwt.validation.client.impl.GwtSpecificValidator} with
   * Generic parameter of the bean type.
   */
  private void writeInterface(GeneratorContext context, TreeLogger logger, BeanHelper bean) {
    PrintWriter pw = context.tryCreate(logger, bean.getPackage(),
        bean.getValidatorName());
    if (pw != null) {
      TreeLogger interfaceLogger = logger.branch(TreeLogger.TRACE,
          "Creating the interface for " + bean.getFullyQualifiedValidatorName());

      ClassSourceFileComposerFactory factory = new ClassSourceFileComposerFactory(
          bean.getPackage(), bean.getValidatorName());
      factory.addImplementedInterface(GwtSpecificValidator.class.getCanonicalName()
          + " <" + bean.getTypeCanonicalName() + ">");
      factory.addImport(GWT.class.getCanonicalName());
      factory.makeInterface();
      SourceWriter sw = factory.createSourceWriter(context, pw);

      // static MyValidator INSTANCE = GWT.create(MyValidator.class);
      sw.print("static ");
      sw.print(bean.getValidatorName());
      sw.print(" INSTANCE = GWT.create(");
      sw.print(bean.getValidatorName());
      sw.println(".class);");

      sw.commit(interfaceLogger);
      pw.close();
    }
  }
}
