package org.jetbrains.plugins.javaFX.fxml.codeInsight.inspections;

import com.intellij.codeInsight.daemon.impl.analysis.RemoveAttributeIntentionFix;
import com.intellij.codeInsight.daemon.impl.analysis.RemoveTagIntentionFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.XmlSuppressableInspectionTool;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.psi.*;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.reference.SoftReference;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.javaFX.fxml.FxmlConstants;
import org.jetbrains.plugins.javaFX.fxml.JavaFxFileTypeFactory;
import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;
import org.jetbrains.plugins.javaFX.fxml.descriptors.JavaFxPropertyAttributeDescriptor;
import org.jetbrains.plugins.javaFX.fxml.descriptors.JavaFxPropertyTagDescriptor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.net.URL;
import java.util.Collections;
import java.util.Map;

/**
 * @author Pavel.Dolgov
 */
public class JavaFxRedundantPropertyValueInspection extends XmlSuppressableInspectionTool {
  private static final Logger LOG = Logger.getInstance("#" + JavaFxRedundantPropertyValueInspection.class.getName());

  private static Reference<Map<String, Map<String, String>>> ourDefaultPropertyValues;

  @NotNull
  @Override
  public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
    return new XmlElementVisitor() {
      @Override
      public void visitXmlFile(XmlFile file) {
        if (!JavaFxFileTypeFactory.isFxml(file)) return;
        super.visitXmlFile(file);
      }

      @Override
      public void visitXmlAttribute(XmlAttribute attribute) {
        super.visitXmlAttribute(attribute);
        final XmlAttributeDescriptor descriptor = attribute.getDescriptor();
        if (!(descriptor instanceof JavaFxPropertyAttributeDescriptor)) return;
        final String attributeName = attribute.getName();
        final String attributeValue = attribute.getValue();
        if (attributeValue == null ||
            attributeValue.startsWith("$") ||
            attributeValue.startsWith("#") ||
            attributeValue.startsWith("%") ||
            FxmlConstants.FX_ID.equals(attributeName) ||
            FxmlConstants.FX_VALUE.equals(attributeName) ||
            FxmlConstants.FX_CONSTANT.equals(attributeName) ||
            FxmlConstants.FX_CONTROLLER.equals(attributeName)) {
          return;
        }
        final PsiClass tagClass = JavaFxPsiUtil.getTagClass(attribute.getParent());
        final String defaultValue = getDefaultValue(attributeName, tagClass);
        if (defaultValue == null) return;

        if (isEqualValue(tagClass, attributeValue, defaultValue, descriptor.getDeclaration())) {
          holder.registerProblem(attribute, "Attribute is redundant because it contains default value",
                                 ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                 new RemoveAttributeIntentionFix(attributeName, attribute));
        }
      }

      @Override
      public void visitXmlTag(XmlTag tag) {
        super.visitXmlTag(tag);
        final XmlElementDescriptor descriptor = tag.getDescriptor();
        if (!(descriptor instanceof JavaFxPropertyTagDescriptor)) {
          return;
        }
        if (tag.getSubTags().length != 0) return;
        final String tagText = tag.getValue().getTrimmedText();
        if (tagText.startsWith("$") ||
            tagText.startsWith("#") ||
            tagText.startsWith("%")) {
          return;
        }

        final XmlTag parentTag = tag.getParentTag();
        if (parentTag == null) return;
        final PsiClass tagClass = JavaFxPsiUtil.getTagClass(parentTag);
        final String defaultValue = getDefaultValue(tag.getName(), tagClass);
        if (defaultValue == null) return;

        if (isEqualValue(tagClass, tagText, defaultValue, descriptor.getDeclaration())) {
          holder.registerProblem(tag, "Tag is redundant because it contains default value",
                                 ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
                                 new RemoveTagIntentionFix(tag.getName(), tag));
        }
      }
    };
  }

  @Nullable
  private static String getDefaultValue(@NotNull String propertyName, @Nullable PsiClass containingClass) {
    for (PsiClass psiClass = containingClass; psiClass != null; psiClass = psiClass.getSuperClass()) {
      final String qualifiedName = psiClass.getQualifiedName();
      if (CommonClassNames.JAVA_LANG_OBJECT.equals(qualifiedName)) break;
      final String defaultValue = getDefaultPropertyValue(qualifiedName, propertyName);
      if (defaultValue != null) {
        return defaultValue;
      }
    }
    return null;
  }

  private static boolean isEqualValue(@Nullable PsiClass containingClass,
                                      @NotNull String attributeValue,
                                      @NotNull String defaultValue,
                                      @Nullable PsiElement declaration) {
    if (!(declaration instanceof PsiMember)) return false;
    final String boxedQName = JavaFxPsiUtil.getBoxedPropertyType(containingClass, (PsiMember)declaration);
    if (boxedQName == null) {
      return defaultValue.equals(attributeValue);
    }
    try {
      switch (boxedQName) {
        case CommonClassNames.JAVA_LANG_BOOLEAN:
          return Boolean.parseBoolean(defaultValue) == Boolean.parseBoolean(attributeValue);
        case CommonClassNames.JAVA_LANG_DOUBLE:
          return Double.compare(Double.parseDouble(defaultValue), Double.parseDouble(attributeValue)) == 0;
        case CommonClassNames.JAVA_LANG_FLOAT:
          return Float.compare(Float.parseFloat(defaultValue), Float.parseFloat(attributeValue)) == 0;
        case CommonClassNames.JAVA_LANG_INTEGER:
          return Integer.parseInt(defaultValue) == Integer.parseInt(attributeValue);
        case CommonClassNames.JAVA_LANG_LONG:
          return Long.parseLong(defaultValue) == Long.parseLong(attributeValue);
        case CommonClassNames.JAVA_LANG_SHORT:
          return Short.parseShort(defaultValue) == Short.parseShort(attributeValue);
        case CommonClassNames.JAVA_LANG_BYTE:
          return Byte.parseByte(defaultValue) == Byte.parseByte(attributeValue);
        default:
          return defaultValue.equals(attributeValue);
      }
    }
    catch (NumberFormatException ignored) {
      return false;
    }
  }

  @Nullable
  private static String getDefaultPropertyValue(String classQualifiedName, String propertyName) {
    final Map<String, String> values = getDefaultPropertyValues(classQualifiedName);
    return values != null ? values.get(propertyName) : null;
  }

  /**
   * Load property values resource. The resource is produced with the script JavaFxGenerateDefaultPropertyValuesScript (can be found in tests)
   */
  @Nullable
  private static Map<String, String> getDefaultPropertyValues(String classQualifiedName) {
    Map<String, Map<String, String>> values = SoftReference.dereference(ourDefaultPropertyValues);
    if (values == null) {
      values = loadDefaultPropertyValues(JavaFxRedundantPropertyValueInspection.class.getSimpleName() + "8.txt");
      ourDefaultPropertyValues = new SoftReference<>(values);
    }
    return values.get(classQualifiedName);
  }

  /**
   * The file format is <code>ClassName#propertyName:type=value</code> per line, line with leading double dash (--) is commented out
   */
  @NotNull
  private static Map<String, Map<String, String>> loadDefaultPropertyValues(@NotNull String resourceName) {
    final URL resource = JavaFxRedundantPropertyValueInspection.class.getResource(resourceName);
    if (resource == null) {
      LOG.warn("Resource not found: " + resourceName);
      return Collections.emptyMap();
    }

    final Map<String, Map<String, String>> result = new THashMap<>(200);
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream(), CharsetToolkit.UTF8_CHARSET))) {
      for (String line : FileUtil.loadLines(reader)) {
        if (line.isEmpty() || line.startsWith("--")) continue;
        boolean lineParsed = false;
        final int p1 = line.indexOf('#');
        if (p1 > 0 && p1 < line.length()) {
          final String className = line.substring(0, p1);
          final int p2 = line.indexOf('=', p1);
          if (p2 > p1 && p2 < line.length()) {
            final String propertyName = line.substring(p1 + 1, p2);
            final String valueText = line.substring(p2 + 1);
            lineParsed = true;
            final Map<String, String> properties = result.computeIfAbsent(className, ignored -> new THashMap<>());
            if (properties.put(propertyName, valueText) != null) {
              LOG.warn("Duplicate default property value " + line);
            }
          }
        }
        if (!lineParsed) {
          LOG.warn("Can't parse default property value " + line);
        }
      }
    }
    catch (IOException e) {
      LOG.warn("Can't read resource: " + resourceName, e);
      return Collections.emptyMap();
    }
    return result;
  }
}

