content: /*******************************************************************************
   * Copyright (C) 2021, 1C-Soft LLC and others.
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   *
   * Contributors:
   *     1C-Soft LLC - initial API and implementation
   *******************************************************************************/
  package com.e1c.v8codestyle.bsl.strict.check;
  
  import java.util.ArrayDeque;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Deque;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Objects;
  import java.util.Set;
  import java.util.TreeSet;
  import java.util.stream.Collectors;
  
  import org.eclipse.core.resources.IProject;
  import org.eclipse.core.runtime.IProgressMonitor;
  import org.eclipse.emf.common.util.URI;
  import org.eclipse.emf.ecore.EObject;
  import org.eclipse.emf.ecore.util.EcoreUtil;
  import org.eclipse.xtext.EcoreUtil2;
  import org.eclipse.xtext.naming.IQualifiedNameConverter;
  import org.eclipse.xtext.naming.QualifiedName;
  import org.eclipse.xtext.resource.IEObjectDescription;
  import org.eclipse.xtext.resource.IResourceServiceProvider;
  import org.eclipse.xtext.scoping.IScope;
  import org.eclipse.xtext.scoping.IScopeProvider;
  
  import com._1c.g5.v8.bm.core.IBmTransaction;
  import com._1c.g5.v8.bm.integration.IBmModel;
  import com._1c.g5.v8.dt.bsl.common.IBslPreferences;
  import com._1c.g5.v8.dt.bsl.documentation.comment.BslMultiLineCommentDocumentationProvider;
  import com._1c.g5.v8.dt.bsl.model.DynamicFeatureAccess;
  import com._1c.g5.v8.dt.bsl.model.ExplicitVariable;
  import com._1c.g5.v8.dt.bsl.model.FeatureAccess;
  import com._1c.g5.v8.dt.bsl.model.FeatureEntry;
  import com._1c.g5.v8.dt.bsl.model.Invocation;
  import com._1c.g5.v8.dt.bsl.model.Method;
  import com._1c.g5.v8.dt.bsl.model.SimpleStatement;
  import com._1c.g5.v8.dt.bsl.model.StaticFeatureAccess;
  import com._1c.g5.v8.dt.bsl.model.Variable;
  import com._1c.g5.v8.dt.bsl.resource.DynamicFeatureAccessComputer;
  import com._1c.g5.v8.dt.bsl.resource.TypesComputer;
  import com._1c.g5.v8.dt.bsl.typesystem.util.TypeSystemUtil;
  import com._1c.g5.v8.dt.core.platform.IBmModelManager;
  import com._1c.g5.v8.dt.core.platform.IDtProject;
  import com._1c.g5.v8.dt.core.platform.IResourceLookup;
  import com._1c.g5.v8.dt.core.platform.IV8Project;
  import com._1c.g5.v8.dt.core.platform.IV8ProjectManager;
  import com._1c.g5.v8.dt.mcore.Environmental;
  import com._1c.g5.v8.dt.mcore.McorePackage;
  import com._1c.g5.v8.dt.mcore.Type;
  import com._1c.g5.v8.dt.mcore.TypeItem;
  import com._1c.g5.v8.dt.mcore.TypeSet;
  import com._1c.g5.v8.dt.mcore.util.Environments;
  import com._1c.g5.v8.dt.mcore.util.McoreUtil;
  import com._1c.g5.v8.dt.platform.IEObjectTypeNames;
  import com.e1c.g5.dt.core.api.naming.INamingService;
  import com.e1c.g5.dt.core.api.platform.BmOperationContext;
  import com.e1c.g5.v8.dt.check.ICheckParameters;
  import com.e1c.g5.v8.dt.check.components.BasicCheck;
  import com.e1c.v8codestyle.internal.bsl.BslPlugin;
  
  /**
   * Abstract check of types in module objects. Allows to compute types respecting system enums,
   * compare intersections of type collections and etc.
   *
   * @author Dmitriy Marmyshev
   */
  public abstract class AbstractTypeCheck
      extends BasicCheck
  {
      private static final String COMMON_MODULE = "CommonModule"; //$NON-NLS-1$
  
      private static final QualifiedName QN_COMMON_MODULE = QualifiedName.create(COMMON_MODULE);
  
      /** The resource lookup service. */
      protected final IResourceLookup resourceLookup;
  
      /** The naming service. */
      protected final INamingService namingService;
  
      /** BM manager service. */
      protected final IBmModelManager bmModelManager;
  
      /** The BSL preferences service. */
      protected final IBslPreferences bslPreferences;
  
      /** The type computer service. */
      protected final TypesComputer typeComputer;
  
      /** The dynamic feature access computer service. */
      protected final DynamicFeatureAccessComputer dynamicFeatureAccessComputer;
  
      /** The scope provider service. */
      protected final IScopeProvider scopeProvider;
  
      /** The qualified name converter service. */
      protected final IQualifiedNameConverter qualifiedNameConverter;
  
      /** The comment provider service. */
      protected final BslMultiLineCommentDocumentationProvider commentProvider;
  
      /**
       * {@link IV8ProjectManager} for getting {@link IV8Project} by {@link EObject}
       */
      protected final IV8ProjectManager v8ProjectManager;
  
      private final InternalTypeNameRegistry internalTypeNameRegistry;
  
      /**
       * Instantiates a new abstract type check.
       *
       * @param resourceLookup the resource lookup service, cannot be {@code null}.
       * @param bslPreferences the BSL preferences service, cannot be {@code null}.
       * @param qualifiedNameConverter the qualified name converter service, cannot be {@code null}.
       * @param namingService service for getting names of EDT object and resources, cannot be <code>null</code>
       * @param bmModelManager service for getting instance of Bm Model by {@link EObject}, cannot be <code>null</code>
       * @param v8ProjectManager {@link IV8ProjectManager} for getting {@link IV8Project} by {@link EObject}, cannot be <code>null</code>
       */
      protected AbstractTypeCheck(IResourceLookup resourceLookup, IBslPreferences bslPreferences,
          IQualifiedNameConverter qualifiedNameConverter, INamingService namingService, IBmModelManager bmModelManager,
          IV8ProjectManager v8ProjectManager)
      {
          super();
          this.resourceLookup = resourceLookup;
          this.namingService = namingService;
          this.bmModelManager = bmModelManager;
          this.bslPreferences = bslPreferences;
          this.v8ProjectManager = v8ProjectManager;
          IResourceServiceProvider rsp =
              IResourceServiceProvider.Registry.INSTANCE.getResourceServiceProvider(URI.createURI("*.bsl")); //$NON-NLS-1$
          this.typeComputer = rsp.get(TypesComputer.class);
          this.dynamicFeatureAccessComputer = rsp.get(DynamicFeatureAccessComputer.class);
          this.scopeProvider = rsp.get(IScopeProvider.class);
          this.commentProvider = rsp.get(BslMultiLineCommentDocumentationProvider.class);
          this.qualifiedNameConverter = qualifiedNameConverter;
          this.internalTypeNameRegistry =
              BslPlugin.getDefault().getInjector().getInstance(InternalTypeNameRegistry.class);
  
      }
  
      @Override
      protected void check(Object object, ResultAcceptor resultAcceptor, ICheckParameters parameters,
          IProgressMonitor progressMonitor)
      {
          IDtProject dtProject = resourceLookup.getDtProject((EObject)object);
          IBmModel bmModel = bmModelManager.getModel(dtProject);
          check(object, resultAcceptor, parameters, bmModel.getEngine().getCurrentTransaction(), progressMonitor);
      }
  
      protected abstract void check(Object object, ResultAcceptor resultAcceptor, ICheckParameters parameters,
          IBmTransaction bmTransaction, IProgressMonitor progressMonitor);
  
      /**
       * Checks if the object has empty types.
       *
       * @param object the object, cannot be {@code null}.
       * @return true, if the object has empty types
       */
      protected boolean isEmptyTypes(EObject object, IBmTransaction bmTransaction)
      {
          Environmental envs = EcoreUtil2.getContainerOfType(object, Environmental.class);
          if (envs == null)
          {
              return true;
          }
  
          Environments actualEnvs = bslPreferences.getLoadEnvs(object).intersect(envs.environments());
          if (actualEnvs.isEmpty())
          {
              return true;
          }
          if (object instanceof Invocation && !actualEnvs.containsAny(Environments.SERVER)
              && ((Invocation)object).isIsServerCall())
          {
              actualEnvs = actualEnvs.add(Environments.SERVER);
          }
  
          List<TypeItem> types = computeTypes(object, actualEnvs);
  
          if (types.isEmpty() && object instanceof ExplicitVariable)
          {
              Collection<TypeItem> commentTypes = computeCommentTypes(object, bmTransaction);
              return commentTypes.isEmpty();
          }
  
          return types.isEmpty();
      }
  
      /**
       * Compute types with respect to system enumeration and variable type state.
       *
       * @param object the object, cannot be {@code null}.
       * @param envs the environments, cannot be {@code null}.
       * @return the list of types, cannot return {@code null}.
       */
      protected List<TypeItem> computeTypes(EObject object, Environments envs)
      {
          if (object instanceof Variable && object.eContainer() instanceof FeatureAccess)
          {
              return TypeSystemUtil.getVariableTypesAfterModelObject((Variable)object, object.eContainer());
          }
          else if (object instanceof StaticFeatureAccess && ((StaticFeatureAccess)object).getImplicitVariable() != null)
          {
              return TypeSystemUtil.getVariableTypesAfterModelObject(((StaticFeatureAccess)object).getImplicitVariable(),
                  object);
          }
  
          List<TypeItem> types = typeComputer.computeTypes(object, envs);
  
          if (types.isEmpty() && object instanceof DynamicFeatureAccess
              && ((DynamicFeatureAccess)object).getSource() instanceof FeatureAccess)
          {
              // Bypass system enum types with property without type
              DynamicFeatureAccess dfa = (DynamicFeatureAccess)object;
              FeatureAccess fa = (FeatureAccess)dfa.getSource();
              List<TypeItem> sourceTypes = typeComputer.computeTypes(fa, envs);
              if (sourceTypes.size() == 1)
              {
                  String typeName = fa.getName();
                  String propertyName = dfa.getName();
                  TypeItem type = sourceTypes.get(0);
                  if (typeName != null
                      && (typeName.equalsIgnoreCase(McoreUtil.getTypeNameRu(type))
                          || typeName.equalsIgnoreCase(McoreUtil.getTypeName(type)))
                      && dynamicFeatureAccessComputer.getAllProperties(sourceTypes, object.eResource())
                          .stream()
                          .flatMap(e -> e.getFirst().stream())
                          .anyMatch(p -> propertyName.equalsIgnoreCase(p.getNameRu())
                              || propertyName.equalsIgnoreCase(p.getName())))
                  {
                      return sourceTypes;
                  }
              }
          }
  
          return types;
      }
  
      /**
       * Compute comment types. This method may be used for {@link SimpleStatement} with variable assignment.
       * In-line documentation comment with type also may be add to the type collection of the right part of
       * the statement.
       *
       * @param object the object, cannot be {@code null}.
       * @return the collection of comment types, cannot return {@code null}.
       */
      protected Collection<TypeItem> computeCommentTypes(EObject object, IBmTransaction bmTransaction)
      {
          IScope typeScope = scopeProvider.getScope(object, McorePackage.Literals.TYPE_DESCRIPTION__TYPES);
          IProject project = resourceLookup.getProject(object);
          boolean oldFormatComment = bslPreferences.getDocumentCommentProperties(project).oldCommentFormat();
          return TypeSystemUtil.computeCommentTypes(object, typeScope, scopeProvider, qualifiedNameConverter,
              commentProvider, v8ProjectManager, oldFormatComment,
              new BmOperationContext(namingService, bmModelManager, bmTransaction));
      }
  
      /**
       * Checks that collections of {@link TypeItem} have common item
       * @param expectedTypes expected collection of {@link TypeItem}, cannot be <code>null</code>
       * @param realTypes real collection of {@link TypeItem}, cannot be <code>null</code>
       * @param context {@link EObject} for resolving proxy checking types, cannot be <code>null</code>
       * @return <code>true</code> if intersection was detected
       */
      protected boolean intersectTypeItem(Collection<TypeItem> expectedTypes, Collection<TypeItem> realTypes,
          EObject context)
      {
          if (expectedTypes.isEmpty())
          {
              return true;
          }
          Collection<String> expectedTypesNames = getTypeNames(expectedTypes, context);
          expectedTypesNames.addAll(getCastingType(expectedTypesNames));
          if (expectedTypesNames.contains(IEObjectTypeNames.ARBITRARY)
              || expectedTypesNames.contains(IEObjectTypeNames.UNDEFINED)
              || expectedTypesNames.contains(IEObjectTypeNames.REFERENCE_TO_OBJECT_OF_INFORMATION_BASE)
              || expectedTypesNames.contains(IEObjectTypeNames.XDTO_DATA_VALUE)
              || expectedTypesNames.contains(IEObjectTypeNames.XDTO_DATA_OBJECT))
          {
              return true;
          }
          if (realTypes.isEmpty())
          {
              return false;
          }
          Collection<TypeItem> withParentTypes = getParentsOfRealTypes(realTypes, context);
          Collection<String> realTypesNames = getTypeNames(withParentTypes, context);
  
          if (!expectedTypesNames.isEmpty() && !realTypesNames.isEmpty())
          {
              expectedTypesNames.retainAll(realTypesNames);
              return !expectedTypesNames.isEmpty();
          }
          else
          {
              return true;
          }
      }
  
      /**
       * Returns a method source for given feature access
       * @param object {@link FeatureAccess}, cannot be <code>null</code>
       * @return the source, can return {@code null}
       */
      protected EObject getSourceMethod(FeatureAccess object)
      {
          Environments actualEnvs = getActualEnvironments(object);
          if (actualEnvs.isEmpty())
          {
              return null;
          }
          List<FeatureEntry> objects = dynamicFeatureAccessComputer.resolveObject(object, actualEnvs);
          for (FeatureEntry entry : objects)
          {
              EObject source = entry.getFeature();
              if (source instanceof Method || (source instanceof com._1c.g5.v8.dt.mcore.Method))
              {
                  return source;
              }
          }
  
          return null;
      }
  
      /**
       * Returns an actual environments for given object
       * @param object {@link EObject}, cannot be <code>null</code>
       * @return the environments, can return {@code Environments.EMPTY} value
       */
      protected Environments getActualEnvironments(EObject object)
      {
          Environmental envs = EcoreUtil2.getContainerOfType(object, Environmental.class);
          if (envs == null)
          {
              return Environments.EMPTY;
          }
  
          return bslPreferences.getLoadEnvs(object).intersect(envs.environments());
      }
  
      private static Collection<? extends String> getCastingType(Collection<String> expectedTypesNames)
      {
          List<String> castTypeNames = new ArrayList<>();
          for (String typeName : expectedTypesNames)
          {
              if (IEObjectTypeNames.STRUCTURE.equals(typeName))
              {
                  castTypeNames.add(IEObjectTypeNames.FIXED_STRUCTURE);
              }
          }
          return castTypeNames;
      }
  
      private Collection<TypeItem> getParentsOfRealTypes(Collection<TypeItem> realTypes, EObject context)
      {
          Deque<TypeItem> types = new ArrayDeque<>(realTypes);
          List<TypeItem> parentTypes = new LinkedList<>();
          while (!types.isEmpty())
          {
              TypeItem type = types.pollFirst();
              type = (TypeItem)EcoreUtil.resolve(type, context);
              parentTypes.add(type);
              if (type instanceof TypeSet)
              {
                  ((TypeSet)type).getTypes().forEach(types::add);
              }
              if (type instanceof Type && ((Type)type).getParentType() != null)
              {
                  types.add(((Type)type).getParentType());
              }
              else if (type instanceof Type && COMMON_MODULE.equals(McoreUtil.getTypeCategory(type)))
              {
                  // Here is bypass of wrong type hierarchy of types for common modules
                  IScope typeScope = scopeProvider.getScope(context, McorePackage.Literals.TYPE_DESCRIPTION__TYPES);
                  IEObjectDescription element = typeScope.getSingleElement(QN_COMMON_MODULE);
                  if (element != null)
                  {
                      EObject parentCommonModuleType = element.getEObjectOrProxy();
                      if (parentCommonModuleType instanceof TypeItem)
                      {
                          parentTypes.add((TypeItem)parentCommonModuleType);
                      }
                  }
              }
          }
          return parentTypes;
      }
  
      private Collection<String> getTypeNames(Collection<TypeItem> parentTypes, EObject context)
      {
          Set<String> typeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
          for (TypeItem type : parentTypes)
          {
              type = (TypeItem)EcoreUtil.resolve(type, context);
              String typeName = McoreUtil.getTypeName(type);
              if (typeName != null)
              {
                  typeNames.add(typeName);
                  String parentTypeName = getTypeSetItemParentTypeName(typeName);
                  if (parentTypeName != null)
                  {
                      typeNames.add(parentTypeName);
                  }
              }
              if (type instanceof TypeSet && typeName != null)
              {
                  typeNames.addAll(((TypeSet)type).getTypes()
                      .stream()
                      .map(McoreUtil::getTypeName)
                      .filter(Objects::nonNull)
                      .collect(Collectors.toList()));
  
                  if (IEObjectTypeNames.ANY_REF.equals(typeName))
                  {
                      typeNames.addAll(internalTypeNameRegistry.allRefTypeSetParentTypeNames());
                  }
              }
          }
          return typeNames;
      }
  
      private String getTypeSetItemParentTypeName(String typeName)
      {
          return internalTypeNameRegistry.getInternalTypeName(typeName);
      }
  
  }
  