/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2023 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.webui.ext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import javax.activation.UnsupportedDataTypeException;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.webui.core.UIComponent;
import org.exoplatform.webui.ext.filter.UIExtensionFilter;
import org.exoplatform.webui.ext.filter.UIExtensionFilters;

/**
 * A Pojo that describes an UI extension
 * 
 * Created by The eXo Platform SAS
 * Author : eXoPlatform
 *          nicolas.filotto@exoplatform.com
 * May 04, 2009  
 */
public class UIExtension implements Comparable<UIExtension> {

  /**
   * Logger.
   */
  private static final Log LOG = ExoLogger.getLogger(UIExtension.class);

  /**
   * Indicates whether the extension is enabled
   */
  private boolean enable = true;
  
  /**
   * The type of the extension, usually it is the FQN of the component 
   * that contains the extension
   */
  private String type;

  /**
   * The name of the extension
   */
  private String name;
  
  /**
   * The category of the extension
   */
  private String category;
  
  /**
   * The rank of the extension
   */
  private int rank;
  
  /**
   * The filters to add 
   */
  private List<UIExtensionFilter> extendedFilters;

  /**
   * The FQN of the component that displays the extension
   */
  private String component;

  /**
   * The class name of the component that displays the extension
   */
  private Class<? extends UIComponent> componentClass;
  
  /**
   * The internal filters of the component
   */
  private List<UIExtensionFilter> componentFilters;
  
  /**
   * Indicates whether the component filters have already been initialized
   */
  private boolean componentFiltersInitialized;
  
  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }
  
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getCategory() {
    return category;
  }

  public void setCategory(String category) {
    this.category = category;
  }

  public int getRank() {
    return rank;
  }

  public void setRank(int rank) {
    this.rank = rank;
  }
  
  public List<UIExtensionFilter> getExtendedFilters() {
    return extendedFilters;
  }

  public void setExtendedFilters(List<UIExtensionFilter> extendedFilters) {
    this.extendedFilters = extendedFilters;
  }
  
  public Class<? extends UIComponent> getComponent() {
    if (componentClass == null) {
      try {
        this.componentClass = Class.forName(component,
                                            false,
                                            Thread.currentThread().getContextClassLoader())
                                   .asSubclass(UIComponent.class);
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("The class of the extension component cannot be found", e);
      }
    }
    return componentClass;
  }

  public void setComponent(Class<? extends UIComponent> componentClass) {
    this.componentClass = componentClass;
  }

  public void setComponent(String component) {
    this.component = component;
    this.componentClass = null;
    this.componentFiltersInitialized = false;
    this.componentFilters = null;
    this.enable = true;
  }

  public List<UIExtensionFilter> getComponentFilters() {
    if (!componentFiltersInitialized) {
      try {
        Class<?> currentClass = getComponent();
        while (currentClass != null) {
          for (Method m : currentClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(UIExtensionFilters.class)) {
              checkMethodReturnType(m);
              @SuppressWarnings("unchecked") 
              List<UIExtensionFilter> filters = (List<UIExtensionFilter>) m.invoke(getComponent().newInstance(), (Object[]) null);
              if (filters != null) {
                // prevent from any undesired modification
                this.componentFilters = Collections.unmodifiableList(filters);              
              }
              break;
            }
          }
          currentClass = currentClass.getSuperclass();
        }
      } catch (SecurityException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } catch (IllegalArgumentException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } catch (IllegalAccessException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } catch (InvocationTargetException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } catch (InstantiationException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } catch (UnsupportedDataTypeException e) {
        // disable the extension to ensure that it won't be used
        this.enable = false;
        LOG.error("The internal filters of the component cannot be initialized", e);
      } finally {
        this.componentFiltersInitialized = true;        
      }
    }
    return componentFilters;
  }    
  
  /**
   * Checks the return type of the method that has been annotated with @UIExtensionFilters
   * it must be a list of objects of type UIExtensionFilter
   * @param m the method to check
   * @throws UnsupportedDataTypeException 
   */
  private void checkMethodReturnType(Method m) throws UnsupportedDataTypeException {
    // Check the return type
    final Type returnType = m.getGenericReturnType();
    if (returnType instanceof ParameterizedType) {
      // The return type is ParameterizedType
      final ParameterizedType pReturnType = (ParameterizedType) returnType;
      final Type rawType = pReturnType.getRawType();
      if (typeEquals(rawType, List.class)) {
        // The raw type is a List
        final Type[] actualTypeArguments = pReturnType.getActualTypeArguments();
        if (actualTypeArguments != null && actualTypeArguments.length == 1
            && typeEquals(actualTypeArguments[0], UIExtensionFilter.class)) {
          // The type argument is UIExtensionFilter, the return type is valid
          return;
        }
      }
    }
    throw new UnsupportedDataTypeException(String.format("The expected type is a list of objects of type UIExtensionFilter. found: %s", returnType));
  }
  
  /**
   * Check if the given type and the given class are the same
   * @param type the type to check
   * @param targetClass the expected class
   * @return <code>true</code> if type equals target class, <code>false</code> otherwise
   */
  @SuppressWarnings("unchecked")
  private boolean typeEquals(Type type, Class targetClass) {
    return type instanceof Class && ((Class) type).getName().equals(targetClass.getName());
  }
  
  public boolean isEnable() {
    return enable;
  }

  /**
   * {@inheritDoc}
   */
  public int compareTo(UIExtension extension) {
    int diff = 0;
    if (rank != 0 && extension.rank != 0) {
      diff = rank - extension.rank;
    } else if (rank == 0 && extension.rank != 0) {
      return 1;
    } else if (rank != 0 && extension.rank == 0) {
      return -1;
    }
    if (diff == 0) {
      if (category != null && extension.category != null) {
        diff = category.compareTo(extension.category);
      } else if (category == null && extension.category != null) {
        return 1;
      } else if (category != null && extension.category == null) {
        return -1;
      }
      if (diff == 0) {
        return name.compareTo(extension.name);
      }
    }
    return diff;
  }
  
  /**
   * {@inheritDoc}
   */
  public boolean equals(Object o) {
      return o instanceof UIExtension && compareTo((UIExtension) o) == 0;
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, category, rank);
  }
}
