/*******************************************************************************
 * 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.olingo.odata2.core.ep.aggregator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.olingo.odata2.api.edm.EdmComplexType;
import org.apache.olingo.odata2.api.edm.EdmConcurrencyMode;
import org.apache.olingo.odata2.api.edm.EdmCustomizableFeedMappings;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmEntityType;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.edm.EdmFunctionImport;
import org.apache.olingo.odata2.api.edm.EdmNavigationProperty;
import org.apache.olingo.odata2.api.edm.EdmProperty;
import org.apache.olingo.odata2.api.edm.EdmSimpleType;
import org.apache.olingo.odata2.api.edm.EdmStructuralType;
import org.apache.olingo.odata2.api.edm.EdmTargetPath;
import org.apache.olingo.odata2.api.edm.EdmType;
import org.apache.olingo.odata2.api.edm.EdmTypeKind;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.api.uri.ExpandSelectTreeNode;

/**
 * Aggregator to get easy and fast access to all for serialization and de-serialization necessary {@link EdmEntitySet}
 * informations.
 * 
 * 
 */
public class EntityInfoAggregator {

  private static final Set<String> SYN_TARGET_PATHS = new HashSet<String>(Arrays.asList(
      EdmTargetPath.SYNDICATION_AUTHORNAME,
      EdmTargetPath.SYNDICATION_AUTHOREMAIL,
      EdmTargetPath.SYNDICATION_AUTHORURI,
      EdmTargetPath.SYNDICATION_PUBLISHED,
      EdmTargetPath.SYNDICATION_RIGHTS,
      EdmTargetPath.SYNDICATION_TITLE,
      EdmTargetPath.SYNDICATION_UPDATED,
      EdmTargetPath.SYNDICATION_CONTRIBUTORNAME,
      EdmTargetPath.SYNDICATION_CONTRIBUTOREMAIL,
      EdmTargetPath.SYNDICATION_CONTRIBUTORURI,
      EdmTargetPath.SYNDICATION_SOURCE,
      EdmTargetPath.SYNDICATION_SUMMARY));

  private Map<String, EntityPropertyInfo> propertyInfo = new HashMap<String, EntityPropertyInfo>();
  private Map<String, NavigationPropertyInfo> navigationPropertyInfos = new HashMap<String, NavigationPropertyInfo>();
  private List<EntityPropertyInfo> keyPropertyInfos;

  /*
   * list with all property names in the order based on order in {@link EdmProperty} (normally [key, entity,
   * navigation])
   */
  private List<String> etagPropertyNames = new ArrayList<String>();
  private List<String> propertyNames;
  private List<String> navigationPropertyNames;
  private List<String> selectedPropertyNames;
  private List<String> selectedNavigationPropertyNames;
  private List<String> expandedNavigationPropertyNames;

  private Map<String, EntityPropertyInfo> targetPath2EntityPropertyInfo = new HashMap<String, EntityPropertyInfo>();
  private List<String> noneSyndicationTargetPaths = new ArrayList<String>();

  private boolean isDefaultEntityContainer;
  private String entitySetName;
  private String entityContainerName;

  private EdmEntityType entityType;
  private EdmEntitySet entitySet;

  /**
   * Constructor is private to force creation over {@link #create(EdmEntitySet)} method.
   */
  private EntityInfoAggregator() {}

  /**
   * Create an {@link EntityInfoAggregator} based on given {@link EdmEntitySet}
   * 
   * @param entitySet
   * with which the {@link EntityInfoAggregator} is initialized.
   * @param expandSelectTree
   * @return created and initialized {@link EntityInfoAggregator}
   * @throws EntityProviderException
   * if during initialization of {@link EntityInfoAggregator} something goes wrong (e.g. exceptions during
   * access
   * of {@link EdmEntitySet}).
   */
  public static EntityInfoAggregator create(final EdmEntitySet entitySet, final ExpandSelectTreeNode expandSelectTree)
      throws EntityProviderException {
    EntityInfoAggregator eia = new EntityInfoAggregator();
    eia.initialize(entitySet, expandSelectTree);
    return eia;
  }

  /**
   * Create an {@link EntityInfoAggregator} based on given {@link EdmEntitySet}
   * 
   * @param entitySet
   * with which the {@link EntityInfoAggregator} is initialized.
   * @return created and initialized {@link EntityInfoAggregator}
   * @throws EntityProviderException
   * if during initialization of {@link EntityInfoAggregator} something goes wrong (e.g. exceptions during
   * access
   * of {@link EdmEntitySet}).
   */
  public static EntityInfoAggregator create(final EdmEntitySet entitySet) throws EntityProviderException {
    EntityInfoAggregator eia = new EntityInfoAggregator();
    eia.initialize(entitySet, null);
    return eia;
  }

  /**
   * Create an {@link EntityPropertyInfo} based on given {@link EdmProperty}
   * 
   * @param property
   * for which the {@link EntityPropertyInfo} is created.
   * @return created {@link EntityPropertyInfo}
   * @throws EntityProviderException
   * if create of {@link EntityPropertyInfo} something goes wrong (e.g. exceptions during
   * access of {@link EdmProperty}).
   */
  public static EntityPropertyInfo create(final EdmProperty property) throws EntityProviderException {
    try {
      EntityInfoAggregator eia = new EntityInfoAggregator();
      return eia.createEntityPropertyInfo(property);
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  /**
   * Create an map of <code>complex type property name</code> to {@link EntityPropertyInfo} based on given
   * {@link EdmComplexType}
   * 
   * @param complexType
   * for which the {@link EntityPropertyInfo} is created.
   * @return created map of <code>complex type property name</code> to {@link EntityPropertyInfo}
   * @throws EntityProviderException
   * if create of {@link EntityPropertyInfo} something goes wrong (e.g. exceptions during
   * access of {@link EntityPropertyInfo}).
   */
  public static Map<String, EntityPropertyInfo> create(final EdmComplexType complexType)
      throws EntityProviderException {
    try {
      EntityInfoAggregator entityInfo = new EntityInfoAggregator();
      return entityInfo.createPropertyInfoObjects(complexType, complexType.getPropertyNames());
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  /**
   * Create an {@link EntityPropertyInfo} based on given {@link EdmFunctionImport}
   * 
   * @param functionImport
   * for which the {@link EntityPropertyInfo} is created.
   * @return created {@link EntityPropertyInfo}
   * @throws EntityProviderException
   * if create of {@link EntityPropertyInfo} something goes wrong (e.g. exceptions during
   * access of {@link EdmFunctionImport}).
   */
  public static EntityPropertyInfo create(final EdmFunctionImport functionImport) throws EntityProviderException {
    try {
      EntityInfoAggregator eia = new EntityInfoAggregator();
      return eia.createEntityPropertyInfo(functionImport, functionImport.getReturnType().getType());
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  /**
   * @return the edm entity set which was used to build this entity info aggregator object
   */
  public EdmEntitySet getEntitySet() {
    return entitySet;
  }

  /**
   * @return entity set name.
   */
  public String getEntitySetName() {
    return entitySetName;
  }

  /**
   * @return <code>true</code> if the entity container of {@link EdmEntitySet} is the default container,
   * otherwise <code>false</code>.
   */
  public boolean isDefaultEntityContainer() {
    return isDefaultEntityContainer;
  }

  public EntityPropertyInfo getTargetPathInfo(final String targetPath) {
    return targetPath2EntityPropertyInfo.get(targetPath);
  }

  public EdmEntityType getEntityType() {
    return entityType;
  }

  public String getEntityContainerName() {
    return entityContainerName;
  }

  /**
   * @return unmodifiable set of all found target path names.
   */
  public Collection<String> getTargetPathNames() {
    return Collections.unmodifiableCollection(targetPath2EntityPropertyInfo.keySet());
  }

  /**
   * @return unmodifiable set of found <code>none syndication target path names</code> (all target path names which are
   * not pre-defined).
   */
  public List<String> getNoneSyndicationTargetPathNames() {
    return Collections.unmodifiableList(noneSyndicationTargetPaths);
  }

  /**
   * @return unmodifiable set of all found navigation property names.
   */
  public List<String> getNavigationPropertyNames() throws EntityProviderException {
    return Collections.unmodifiableList(navigationPropertyNames);
  }

  /**
   * @return unmodifiable set of all property names.
   */
  public List<String> getPropertyNames() throws EntityProviderException {
    return Collections.unmodifiableList(propertyNames);
  }

  /**
   * @return unmodifiable set of selected property names.
   */
  public List<String> getSelectedPropertyNames() throws EntityProviderException {
    return Collections.unmodifiableList(selectedPropertyNames);
  }

  /**
   * @return unmodifiable set of selected property names.
   */
  public List<String> getSelectedNavigationPropertyNames() throws EntityProviderException {
    return Collections.unmodifiableList(selectedNavigationPropertyNames);
  }

  public Collection<EntityPropertyInfo> getPropertyInfos() {
    return Collections.unmodifiableCollection(propertyInfo.values());
  }

  public EntityPropertyInfo getPropertyInfo(final String name) {
    return propertyInfo.get(name);
  }

  public Collection<EntityPropertyInfo> getETagPropertyInfos() {
    List<EntityPropertyInfo> keyProperties = new ArrayList<EntityPropertyInfo>();
    for (String etagPropertyName : etagPropertyNames) {
      EntityPropertyInfo e = propertyInfo.get(etagPropertyName);
      keyProperties.add(e);
    }
    return keyProperties;
  }

  /**
   * @return list of all key property infos
   * @throws EntityProviderException
   */
  public List<EntityPropertyInfo> getKeyPropertyInfos() throws EntityProviderException {

    if (keyPropertyInfos == null) {
      try {
        keyPropertyInfos = new ArrayList<EntityPropertyInfo>();
        for (String keyPropertyName : entityType.getKeyPropertyNames()) {
          keyPropertyInfos.add(propertyInfo.get(keyPropertyName));
        }
      } catch (EdmException e) {
        throw new EntityProviderException(EntityProviderException.COMMON, e);
      }
    }
    return keyPropertyInfos;
  }

  public NavigationPropertyInfo getNavigationPropertyInfo(final String name) {
    return navigationPropertyInfos.get(name);
  }

  private void initialize(final EdmEntitySet entitySet, final ExpandSelectTreeNode expandSelectTree)
      throws EntityProviderException {
    try {
      this.entitySet = entitySet;
      entityType = entitySet.getEntityType();
      entitySetName = entitySet.getName();
      isDefaultEntityContainer = entitySet.getEntityContainer().isDefaultEntityContainer();
      entityContainerName = entitySet.getEntityContainer().getName();

      propertyNames = entityType.getPropertyNames();
      navigationPropertyNames = entityType.getNavigationPropertyNames();

      propertyInfo = createPropertyInfoObjects(entityType, propertyNames);
      navigationPropertyInfos = createNavigationInfoObjects(entityType, navigationPropertyNames);

      selectedPropertyNames = propertyNames;
      selectedNavigationPropertyNames = navigationPropertyNames;
      expandedNavigationPropertyNames = new ArrayList<String>();

      if (expandSelectTree != null && !expandSelectTree.isAll()) {
        selectedPropertyNames = new ArrayList<String>();
        selectedNavigationPropertyNames = new ArrayList<String>();
        for (EdmProperty property : expandSelectTree.getProperties()) {
          selectedPropertyNames.add(property.getName());
        }
        for (String property : expandSelectTree.getLinks().keySet()) {
          selectedNavigationPropertyNames.add(property);
          if (expandSelectTree.getLinks().get(property) != null) {
            expandedNavigationPropertyNames.add(property);
          }
        }
      } else if (expandSelectTree != null) {
        for (String property : expandSelectTree.getLinks().keySet()) {
          if (expandSelectTree.getLinks().get(property) != null) {
            expandedNavigationPropertyNames.add(property);
          }
        }
      }

    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  private Map<String, EntityPropertyInfo> createPropertyInfoObjects(final EdmStructuralType type,
      final List<String> propertyNames) throws EntityProviderException {
    try {
      Map<String, EntityPropertyInfo> infos = new HashMap<String, EntityPropertyInfo>();

      for (String propertyName : propertyNames) {
        EdmProperty property = (EdmProperty) type.getProperty(propertyName);

        checkETagRelevant(property);

        EntityPropertyInfo info = createEntityPropertyInfo(property);
        infos.put(info.getName(), info);
        checkTargetPathInfo(property, info);
      }

      return infos;
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  private Map<String, NavigationPropertyInfo> createNavigationInfoObjects(final EdmStructuralType type,
      final List<String> propertyNames) throws EntityProviderException {
    try {
      Map<String, NavigationPropertyInfo> infos = new HashMap<String, NavigationPropertyInfo>();

      for (String propertyName : propertyNames) {
        EdmNavigationProperty navProperty = (EdmNavigationProperty) type.getProperty(propertyName);
        NavigationPropertyInfo info = NavigationPropertyInfo.create(navProperty);
        infos.put(propertyName, info);
      }

      return infos;
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  private EntityPropertyInfo createEntityPropertyInfo(final EdmProperty property) throws EdmException,
      EntityProviderException {
    EdmType type = property.getType();
    if (type instanceof EdmSimpleType) {
      return EntityPropertyInfo.create(property);
    } else if (type instanceof EdmComplexType) {
      EdmComplexType complex = (EdmComplexType) type;
      Map<String, EntityPropertyInfo> recursiveInfos = createPropertyInfoObjects(complex, complex.getPropertyNames());
      return EntityComplexPropertyInfo.create(property, complex.getPropertyNames(), recursiveInfos);
    } else {
      throw new EntityProviderException(EntityProviderException.UNSUPPORTED_PROPERTY_TYPE);
    }
  }

  private EntityPropertyInfo createEntityPropertyInfo(final EdmFunctionImport functionImport, final EdmType type)
      throws EdmException, EntityProviderException {
    EntityPropertyInfo epi;

    if (type.getKind() == EdmTypeKind.COMPLEX) {
      EdmComplexType complex = (EdmComplexType) type;
      Map<String, EntityPropertyInfo> eia = EntityInfoAggregator.create(complex);

      List<EntityPropertyInfo> childEntityInfoList = new ArrayList<EntityPropertyInfo>();
      for (String propertyName : complex.getPropertyNames()) {
        childEntityInfoList.add(eia.get(propertyName));
      }
      epi = new EntityComplexPropertyInfo(functionImport.getName(), type, null, null, childEntityInfoList);

    } else if (type.getKind() == EdmTypeKind.SIMPLE) {

      epi = new EntityPropertyInfo(functionImport.getName(), type, null, null, null, null);
    } else {
      throw new EntityProviderException(EntityProviderException.UNSUPPORTED_PROPERTY_TYPE.addContent(type.getKind()));
    }

    return epi;
  }

  private void checkETagRelevant(final EdmProperty edmProperty) throws EntityProviderException {
    try {
      if (edmProperty.getFacets() != null && edmProperty.getFacets().getConcurrencyMode() == EdmConcurrencyMode.Fixed) {
        etagPropertyNames.add(edmProperty.getName());
      }
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  private void checkTargetPathInfo(final EdmProperty property, final EntityPropertyInfo propertyInfo)
      throws EntityProviderException {
    try {
      EdmCustomizableFeedMappings customizableFeedMappings = property.getCustomizableFeedMappings();
      if (customizableFeedMappings != null) {
        String targetPath = customizableFeedMappings.getFcTargetPath();
        targetPath2EntityPropertyInfo.put(targetPath, propertyInfo);
        if (!SYN_TARGET_PATHS.contains(targetPath)) {
          noneSyndicationTargetPaths.add(targetPath);
        }
      }
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.COMMON, e);
    }
  }

  public List<String> getExpandedNavigationPropertyNames() {
    return expandedNavigationPropertyNames;
  }
}
