package org.infinispan.query.objectfilter.impl.syntax.parser;

import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.tree.Tree;
import org.infinispan.query.objectfilter.impl.logging.Log;
import org.infinispan.query.objectfilter.impl.ql.JoinType;
import org.infinispan.query.objectfilter.impl.ql.PropertyPath;
import org.infinispan.query.objectfilter.impl.ql.QueryResolverDelegate;
import org.jboss.logging.Logger;

/**
 * @author anistor@redhat.com
 * @since 7.0
 */
final class QueryResolverDelegateImpl<TypeMetadata> implements QueryResolverDelegate<TypeDescriptor<TypeMetadata>> {

   private static final Log log = Logger.getMessageLogger(MethodHandles.lookup(), Log.class, QueryResolverDelegateImpl.class.getName());

   private final Map<String, String> aliasToEntityType = new HashMap<>();

   private final Map<String, PropertyPath<TypeDescriptor<TypeMetadata>>> aliasToPropertyPath = new HashMap<>();

   private final ObjectPropertyHelper<TypeMetadata> propertyHelper;

   private String targetType;

   private TypeMetadata entityMetadata;

   private String alias;

   private enum Phase {
      SELECT,
      FROM
   }

   private Phase phase;

   QueryResolverDelegateImpl(ObjectPropertyHelper<TypeMetadata> propertyHelper) {
      this.propertyHelper = propertyHelper;
   }

   @Override
   public void registerPersisterSpace(String entityName, Tree aliasTree) {
      String aliasText = aliasTree.getText();
      String prevAlias = aliasToEntityType.put(aliasText, entityName);
      if (prevAlias != null && !prevAlias.equalsIgnoreCase(entityName)) {
         throw new UnsupportedOperationException("Alias reuse currently not supported: alias " + aliasText + " already assigned to type " + prevAlias);
      }
      if (targetType != null) {
         throw new IllegalStateException("Can't target multiple types: " + targetType + " already selected before " + entityName);
      }
      targetType = entityName;
      entityMetadata = propertyHelper.getEntityMetadata(entityName);
      if (entityMetadata == null) {
         throw log.getUnknownEntity(entityName);
      }
   }

   @Override
   public void registerJoinAlias(Tree alias, PropertyPath<TypeDescriptor<TypeMetadata>> path) {
      if (!path.isEmpty() && !aliasToPropertyPath.containsKey(alias.getText())) {
         aliasToPropertyPath.put(alias.getText(), path);
      }
   }

   @Override
   public boolean isUnqualifiedPropertyReference() {
      return true;
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeUnqualifiedPropertyReference(Tree propertyNameTree) {
      String property = propertyNameTree.getText();
      if (aliasToEntityType.containsKey(property)) {
         return normalizeQualifiedRoot(propertyNameTree);
      }

      EntityTypeDescriptor<TypeMetadata> type = new EntityTypeDescriptor<>(targetType, entityMetadata);
      return normalizeProperty(type, Collections.emptyList(), property);
   }

   @Override
   public boolean isPersisterReferenceAlias() {
      return aliasToEntityType.containsKey(alias);
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeUnqualifiedRoot(Tree aliasTree) {
      String alias = aliasTree.getText();
      if (aliasToEntityType.containsKey(alias)) {
         return normalizeQualifiedRoot(aliasTree);
      }

      PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath = aliasToPropertyPath.get(alias);
      if (propertyPath == null) {
         throw log.getUnknownAliasException(alias);
      }

      List<String> resolvedAlias = resolveAlias(propertyPath);
      TypeDescriptor<TypeMetadata> sourceType = propertyPath.getFirstTypeDescriptor();

      EmbeddedEntityTypeDescriptor<TypeMetadata> type = new EmbeddedEntityTypeDescriptor<>(sourceType.getTypeName(), sourceType.getTypeMetadata(), resolvedAlias);
      return new PropertyPath.PropertyReference<>(alias, type, true);
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeQualifiedRoot(Tree root) {
      String alias = root.getText();
      String entityNameForAlias = aliasToEntityType.get(alias);

      if (entityNameForAlias == null) {
         PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath = aliasToPropertyPath.get(alias);
         if (propertyPath == null) {
            throw log.getUnknownAliasException(alias);
         }
         return new PropertyPath.PropertyReference<>(propertyPath.asStringPathWithoutAlias(), null, false);
      }

      TypeMetadata entityMetadata = propertyHelper.getEntityMetadata(entityNameForAlias);
      if (entityMetadata == null) {
         throw log.getUnknownEntity(entityNameForAlias);
      }

      return new PropertyPath.PropertyReference<>(alias, new EntityTypeDescriptor<>(entityNameForAlias, entityMetadata), true);
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizePropertyPathIntermediary(PropertyPath<TypeDescriptor<TypeMetadata>> path, Tree propertyNameTree) {
      String propertyName = propertyNameTree.getText();
      TypeDescriptor<TypeMetadata> sourceType = path.getLastTypeDescriptor();
      if (!propertyHelper.hasProperty(sourceType.getTypeMetadata(), sourceType.makePath(propertyName))) {
         throw log.getNoSuchPropertyException(sourceType.getTypeName(), propertyName);
      }

      List<String> resolvedAlias = resolveAlias(path);
      resolvedAlias.add(propertyName);

      EmbeddedEntityTypeDescriptor<TypeMetadata> type = new EmbeddedEntityTypeDescriptor<>(sourceType.getTypeName(), sourceType.getTypeMetadata(), resolvedAlias);
      return new PropertyPath.PropertyReference<>(propertyName, type, false);
   }

   private List<String> resolveAlias(PropertyPath<TypeDescriptor<TypeMetadata>> path) {
      if (path.isAlias()) {
         String alias = path.getAlias();
         if (aliasToEntityType.containsKey(alias)) {
            // Alias for entity
            return path.getNodeNamesWithoutAlias();
         } else if (aliasToPropertyPath.containsKey(alias)) {
            // Alias for embedded
            PropertyPath<TypeDescriptor<TypeMetadata>> propertyPath = aliasToPropertyPath.get(alias);
            List<String> resolvedAlias = resolveAlias(propertyPath);
            resolvedAlias.addAll(path.getNodeNamesWithoutAlias());
            return resolvedAlias;
         } else {
            // Alias not found
            throw log.getUnknownAliasException(alias);
         }
      }
      // It does not start with an alias
      return path.getNodeNamesWithoutAlias();
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeIntermediateIndexOperation(PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> propertyReference, Tree collectionProperty, Tree selector) {
      throw new UnsupportedOperationException("Not implemented");
   }

   @Override
   public void normalizeTerminalIndexOperation(PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> propertyReference, Tree collectionProperty, Tree selector) {
      throw new UnsupportedOperationException("Not implemented");
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeUnqualifiedPropertyReferenceSource(Tree identifier) {
      throw new UnsupportedOperationException("Not implemented");
   }

   @Override
   public PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizePropertyPathTerminus(PropertyPath<TypeDescriptor<TypeMetadata>> path, Tree propertyNameTree) {
      // receives the property name on a specific entity reference _source_
      return normalizeProperty(path.getLastTypeDescriptor(), path.getNodeNamesWithoutAlias(), propertyNameTree.getText());
   }

   private PropertyPath.PropertyReference<TypeDescriptor<TypeMetadata>> normalizeProperty(TypeDescriptor<TypeMetadata> type, List<String> path, String propertyName) {
      String[] propertyPath = type.makePath(propertyName);
      if (!propertyHelper.hasProperty(type.getTypeMetadata(), propertyPath)) {
         throw log.getNoSuchPropertyException(type.getTypeName(), propertyName);
      }

      TypeDescriptor<TypeMetadata> propType = null;
      if (propertyHelper.hasEmbeddedProperty(type.getTypeMetadata(), propertyPath)) {
         List<String> newPath = new LinkedList<>(path);
         newPath.add(propertyName);
         propType = new EmbeddedEntityTypeDescriptor<>(type.getTypeName(), type.getTypeMetadata(), newPath);
      }
      return new PropertyPath.PropertyReference<>(propertyName, propType, false);
   }

   @Override
   public void activateFromStrategy(JoinType joinType, Tree associationFetchTree, Tree propertyFetchTree, Tree aliasTree) {
      phase = Phase.FROM;
      alias = aliasTree.getText();
   }

   @Override
   public void activateSelectStrategy() {
      phase = Phase.SELECT;
   }

   @Override
   public void activateDeleteStrategy() {
   }

   @Override
   public void deactivateStrategy() {
      phase = null;
      alias = null;
   }

   @Override
   public void propertyPathCompleted(PropertyPath<TypeDescriptor<TypeMetadata>> path) {
      if (phase == Phase.SELECT) {
         TypeDescriptor<TypeMetadata> type = path.getLastTypeDescriptor();
         if (type instanceof EmbeddedEntityTypeDescriptor<?>) {
            throw log.getProjectionOfCompleteEmbeddedEntitiesNotSupportedException(type.getTypeName(), path.asStringPathWithoutAlias());
         }
      }
   }
}
