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;
  
  import java.io.BufferedReader;
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.Reader;
  import java.nio.charset.StandardCharsets;
  import java.util.Iterator;
  
  import org.eclipse.core.resources.IFile;
  import org.eclipse.core.resources.IProject;
  import org.eclipse.core.resources.ProjectScope;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.core.runtime.IProgressMonitor;
  import org.eclipse.core.runtime.Platform;
  import org.eclipse.core.runtime.preferences.ConfigurationScope;
  import org.eclipse.core.runtime.preferences.DefaultScope;
  import org.eclipse.core.runtime.preferences.IScopeContext;
  import org.eclipse.core.runtime.preferences.InstanceScope;
  import org.eclipse.emf.common.notify.impl.AdapterImpl;
  import org.eclipse.emf.ecore.EObject;
  import org.eclipse.emf.ecore.util.EcoreUtil;
  import org.eclipse.xtext.EcoreUtil2;
  import org.eclipse.xtext.nodemodel.ICompositeNode;
  import org.eclipse.xtext.nodemodel.ILeafNode;
  import org.eclipse.xtext.nodemodel.INode;
  import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
  
  import com._1c.g5.v8.dt.bsl.documentation.comment.BslCommentUtils;
  import com._1c.g5.v8.dt.bsl.documentation.comment.IBslCommentToken;
  import com._1c.g5.v8.dt.bsl.model.Module;
  import com._1c.g5.v8.dt.common.PreferenceUtils;
  import com._1c.g5.v8.dt.common.StringUtils;
  import com.e1c.v8codestyle.internal.bsl.BslPlugin;
  import com.google.common.io.CharStreams;
  
  /**
   * The utility class for strict-types system.
   *
   * @author Dmitriy Marmyshev
   */
  public final class StrictTypeUtil
  {
  
      /** The strict-types annotation using in module header to activate checks */
      public static final String STRICT_TYPE_ANNOTATION = "@strict-types"; //$NON-NLS-1$
  
      public static final String BSL_FILE_EXTENSION = "bsl"; //$NON-NLS-1$
  
      /** The preference root qualifier. */
      public static final String PREF_QUALIFIER = BslPlugin.PLUGIN_ID;
  
      /** The key for preferences store the state of the creating module with {@code //@strict-types} annotation */
      public static final String PREF_KEY_CREATE_STRICT_TYPES = "addModuleStrictTypesAnnotation"; //$NON-NLS-1$
  
      /** The default value of creating module with strict types. */
      public static final boolean PREF_DEFAULT_CREATE_STRICT_TYPES = true;
  
      private static final int COMMENT_LENGTH = IBslCommentToken.LINE_STARTER.length();
  
      /**
       * Can add module strict-types annotation for project.
       *
       * @param project the project, cannot be {@code null}.
       * @return true, if can create strict-types module for the project
       */
      public static boolean canAddModuleStrictTypesAnnotation(IProject project)
      {
          ProjectScope scope = new ProjectScope(project);
          IScopeContext[] contexts =
              new IScopeContext[] { scope, InstanceScope.INSTANCE, ConfigurationScope.INSTANCE, DefaultScope.INSTANCE };
  
          return Platform.getPreferencesService()
              .getBoolean(PREF_QUALIFIER, PREF_KEY_CREATE_STRICT_TYPES, PREF_DEFAULT_CREATE_STRICT_TYPES, contexts);
      }
  
      /**
       * Checks for {@code @strict-types} annotation in BSL module file.
       *
       * @param file the file of BSL module, cannot be {@code null}.
       * @return true, if the module has {@code @strict-types} annotation in module header
       * @throws CoreException the core exception
       * @throws IOException Signals that an I/O exception has occurred.
       */
      public static boolean hasStrictTypeAnnotation(IFile file) throws CoreException, IOException
      {
          try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents(), file.getCharset())))
          {
  
              String line = reader.readLine();
              while (line != null)
              {
                  line = line.trim();
                  if (!line.isEmpty() && !line.startsWith(IBslCommentToken.LINE_STARTER))
                  {
                      return false;
                  }
                  if (line.length() > 0 && line.substring(COMMENT_LENGTH).trim().startsWith(STRICT_TYPE_ANNOTATION))
                  {
                      return true;
                  }
  
                  line = reader.readLine();
              }
          }
  
          return false;
      }
  
      /**
       * Checks for {@code @strict-types} annotation in module description.
       *
       * @param object the object of BSL module, cannot be {@code null}.
       * @return true, if module has annotation in header
       */
      public static boolean hasStrictTypeAnnotation(EObject object)
      {
          Module module = EcoreUtil2.getContainerOfType(object, Module.class);
          if (module == null)
          {
              return false;
          }
  
          return hasStrictTypeAnnotation(module);
      }
  
      /**
       * Checks for {@code @strict-types} annotation in module description.
       *
       * @param module the module, cannot be {@code null}.
       * @return true, if module has annotation in header
       */
      public static boolean hasStrictTypeAnnotation(Module module)
      {
          ModuleAnnotationAdapter adapter =
              (ModuleAnnotationAdapter)EcoreUtil.getExistingAdapter(module, ModuleAnnotationAdapter.class);
          if (adapter == null)
          {
              ICompositeNode node = NodeModelUtils.getNode(module);
              node = node.getRootNode();
              ILeafNode annotationNode = getStrictTypeAnnotationNode(node);
              synchronized (module.eAdapters())
              {
                  // double check and add adapter if absent
                  adapter = (ModuleAnnotationAdapter)EcoreUtil.getExistingAdapter(module, ModuleAnnotationAdapter.class);
                  if (adapter == null)
                  {
                      adapter = new ModuleAnnotationAdapter(annotationNode);
                      module.eAdapters().add(adapter);
                  }
              }
          }
  
          return adapter.getStrictTypeAnnotationNode() != null;
      }
  
      /**
       * Checks for {@code @strict-types} annotation in module description.
       *
       * @param root the root, cannot be {@code null}.
       * @return true, if module has annotation in header
       */
      public static boolean hasStrictTypeAnnotation(INode root)
      {
          return getStrictTypeAnnotationNode(root) != null;
      }
  
      /**
       *
       * Gets the {@link ILeafNode} of the  {@code @strict-types} annotation in module description.
       *
       * @param root the root, cannot be {@code null}.
       * @return leaf node, if module has annotation in header, or {@code null} if not found
       */
      public static ILeafNode getStrictTypeAnnotationNode(INode root)
      {
          for (ILeafNode node : root.getLeafNodes())
          {
              if (!node.isHidden())
              {
                  return null;
              }
  
              if (BslCommentUtils.isCommentNode(node)
                  && node.getText().substring(COMMENT_LENGTH).trim().startsWith(STRICT_TYPE_ANNOTATION))
              {
                  return node;
              }
  
          }
          return null;
      }
  
      /**
       * Sets the {@code @strict-types} annotation in the file. If file is not exits this method will create empty file
       * with annotation.
       *
       * @param bslFile the BSL file, cannot be {@code null}.
       * @param monitor the monitor
       * @throws IOException Signals that an I/O exception has occurred.
       * @throws CoreException the core exception
       */
      public static void setStrictTypeAnnotation(IFile bslFile, IProgressMonitor monitor)
          throws IOException, CoreException
      {
          String currentCode = StringUtils.EMPTY;
          if (bslFile.exists())
          {
              try (InputStream in = bslFile.getContents();
                  Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);)
              {
                  currentCode = CharStreams.toString(reader);
              }
          }
  
          String preferedLineSeparator = PreferenceUtils.getLineSeparator(bslFile.getProject());
          StringBuilder sb = new StringBuilder();
  
          int insertOffset = getInsertOffset(currentCode, preferedLineSeparator);
          if (insertOffset > 0)
          {
              sb.append(currentCode.substring(0, insertOffset));
              sb.append(preferedLineSeparator);
          }
  
          sb.append(IBslCommentToken.LINE_STARTER);
          sb.append(" "); //$NON-NLS-1$
          sb.append(StrictTypeUtil.STRICT_TYPE_ANNOTATION);
          sb.append(preferedLineSeparator);
          sb.append(preferedLineSeparator);
          sb.append(currentCode.substring(insertOffset));
  
          if (monitor.isCanceled())
          {
              return;
          }
  
          try (InputStream in = new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8));)
          {
              if (bslFile.exists())
              {
                  bslFile.setContents(in, true, true, monitor);
              }
              else
              {
                  bslFile.create(in, true, monitor);
              }
          }
      }
  
      private static int getInsertOffset(String currentCode, String preferedLineSeparator)
      {
          int separator = preferedLineSeparator.length();
  
          int offset = 0;
          for (Iterator<String> iterator = currentCode.lines().iterator(); iterator.hasNext();)
          {
              if (offset > 0)
              {
                  offset = offset + separator;
              }
  
              String line = iterator.next();
              if (StringUtils.isBlank(line))
              {
                  return offset;
              }
              else if (!line.stripLeading().startsWith(IBslCommentToken.LINE_STARTER))
              {
                  return 0;
              }
  
              offset = offset + line.length();
  
          }
          return 0;
      }
  
      private StrictTypeUtil()
      {
          throw new IllegalAccessError();
      }
  
      private static final class ModuleAnnotationAdapter
          extends AdapterImpl
      {
  
          private final ILeafNode annotationNode;
  
          private ModuleAnnotationAdapter(ILeafNode annotationNode)
          {
              this.annotationNode = annotationNode;
          }
  
          public ILeafNode getStrictTypeAnnotationNode()
          {
              return annotationNode;
          }
  
          @Override
          public boolean isAdapterForType(Object type)
          {
              return getClass() == type;
          }
      }
  
  }
  