/*
 * Copyright (c) 2009 - 2017 - Pierre-Laurent Coirer, Frank Hossfeld
 *
 * 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.mvp4g.rebind.config.loader.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Set;

import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.mvp4g.rebind.config.Mvp4gConfiguration;
import com.mvp4g.rebind.config.element.Mvp4gElement;
import com.mvp4g.rebind.config.element.SimpleMvp4gElement;
import com.mvp4g.rebind.exception.loader.Mvp4gAnnotationException;

/**
 * Base class responsible for loading information contained in annotations. All annotation loader
 * must expand this class.
 *
 * @author plcoirier
 */
public abstract class Mvp4gAnnotationsLoader<T extends Annotation> {

  /**
   * Load information containing in annotations for each class.
   *
   * @param annotedClasses
   *   list of class that are annoted with the annotation
   * @param configuration
   *   configuration containing loaded elements of the application
   *
   * @throws Mvp4gAnnotationException
   *   if annotation is not used correctly
   */
  @SuppressWarnings("unchecked")
  public void load(List<JClassType> annotedClasses,
                   Mvp4gConfiguration configuration)
    throws Mvp4gAnnotationException {
    JClassType mandatoryInterface = configuration.getOracle()
                                                 .findType(getMandatoryInterfaceName());
    for (JClassType c : annotedClasses) {
      controlType(c,
                  mandatoryInterface);
      loadElement(c,
                  c.getAnnotation((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]),
                  configuration);
    }
  }

  /**
   * @return name of the interface that class that has the specific annotation must implement
   */
  abstract protected String getMandatoryInterfaceName();

  /**
   * Control if the class can accept the annotation.
   *
   * @param c
   *   class to control
   * @param mandatoryInterface
   *   interface that the class must implement
   *
   * @throws Mvp4gAnnotationException
   *   if the class don't implement the interface
   */
  @SuppressWarnings("unchecked")
  protected void controlType(JClassType c,
                             JClassType mandatoryInterface)
    throws Mvp4gAnnotationException {
    if (!c.isAssignableTo(mandatoryInterface)) {
      String annotationClassName = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]).getCanonicalName();
      throw new Mvp4gAnnotationException(c.getQualifiedSourceName(),
                                         null,
                                         "this class must implement " + mandatoryInterface.getQualifiedSourceName() + " since it is annoted with " + annotationClassName + ".");
    }
  }

  /**
   * Load one class annoted with the annotation
   *
   * @param c
   *   class annoted with the annotation
   * @param annotation
   *   annotation of the class
   * @param configuration
   *   configuration containing loaded elements of the application
   *
   * @throws Mvp4gAnnotationException
   *   if annotation is not used correctly
   */
  abstract protected void loadElement(JClassType c,
                                      T annotation,
                                      Mvp4gConfiguration configuration)
    throws Mvp4gAnnotationException;

  /**
   * Add an element to the set and verify first if there is a duplicate.
   *
   * @param <E>
   *   type of the element
   * @param loadedElements
   *   set where to add the element
   * @param element
   *   element to add
   * @param c
   *   annoted class that asks for this element to be added (can be null, only for error
   *   information purpose)
   * @param m
   *   annoted method that ask for this element to be added (can be null, only for error
   *   information purpose)
   *
   * @throws Mvp4gAnnotationException
   *   if a duplicate element is already in the list
   */
  protected <E extends Mvp4gElement> void addElement(Set<E> loadedElements,
                                                     E element,
                                                     JClassType c,
                                                     JMethod m)
    throws Mvp4gAnnotationException {
    checkForDuplicates(loadedElements,
                       element,
                       c,
                       m);
    loadedElements.add(element);
  }

  /**
   * Check if an element is already present in a set
   *
   * @param <E>
   *   type of the elements
   * @param loadedElements
   *   set of elements where the check is done
   * @param element
   *   searched element
   * @param c
   *   annoted class that asks for this element to be added (can be null, only for error
   *   information purpose)
   * @param m
   *   annoted method that ask for this element to be added (can be null, only for error
   *   information purpose)
   *
   * @throws Mvp4gAnnotationException
   *   if the element is already in the set
   */
  private <E extends Mvp4gElement> void checkForDuplicates(Set<E> loadedElements,
                                                           E element,
                                                           JClassType c,
                                                           JMethod m)
    throws Mvp4gAnnotationException {
    if (loadedElements.contains(element)) {
      String err = "Duplicate " + element.getTagName() + " identified by " + "'" + element.getUniqueIdentifierName() + "' found in configuration file.";
      throw new Mvp4gAnnotationException(c.getQualifiedSourceName(),
                                         (m == null) ?
                                         null :
                                         m.getName(),
                                         err);
    }
  }

  /**
   * Build a name for the element thanks to its class in case the current one is empty or null.
   *
   * @param currentName
   *   current name of the element
   * @param className
   *   class name of the element
   * @param suffix
   *   suffix to add to the element is needed
   *
   * @return new name of the element if the current name was null or empty, current name otherwise
   */
  protected String buildElementNameIfNeeded(String currentName,
                                            String className,
                                            String suffix) {
    if ((currentName == null) || (currentName.length() == 0)) {
      return buildElementName(className,
                              suffix);
    } else {
      return currentName;
    }
  }

  /**
   * Build a name for the element thanks to its class by replacing '.' by '_'. Also add a suffix
   * to the name.
   *
   * @param className
   *   class name of the element
   * @param suffix
   *   suffix to add at the end of his name
   *
   * @return name of the element
   */
  protected String buildElementName(String className,
                                    String suffix) {
    return className.replace('.',
                             '_') + suffix;
  }

  /**
   * Find the first element with the corresponding class name and return its name.
   *
   * @param <E>
   *   type of the elements
   * @param loadedElements
   *   set of elements to look for
   * @param elementClassName
   *   name of the class of the seached element
   *
   * @return name of the element found, null otherwise
   */
  protected <E extends SimpleMvp4gElement> String getElementName(Set<E> loadedElements,
                                                                 String elementClassName) {
    String elementName = null;
    for (E element : loadedElements) {
      if (elementClassName.equals(element.getClassName())) {
        elementName = element.getName();
        break;
      }
    }
    return elementName;
  }

  /**
   * Find the first element with the corresponding class name.
   *
   * @param <E>
   *   type of the elements
   * @param loadedElements
   *   set of elements to look for
   * @param elementClassName
   *   name of the class of the seached element
   *
   * @return name of the element found, null otherwise
   */
  protected <E extends SimpleMvp4gElement> E getElement(Set<E> loadedElements,
                                                        String elementClassName) {
    for (E element : loadedElements) {
      if (elementClassName.equals(element.getClassName())) {
        return element;
      }
    }
    return null;
  }

}