/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed 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.jetbrains.android.spellchecker;

import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.lint.detector.api.LintUtils;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.spellchecker.inspections.TextSplitter;
import com.intellij.spellchecker.tokenizer.TokenConsumer;
import com.intellij.spellchecker.tokenizer.Tokenizer;
import com.intellij.spellchecker.xml.XmlSpellcheckingStrategy;
import com.intellij.util.xml.Converter;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import com.intellij.util.xml.GenericAttributeValue;
import org.jetbrains.android.dom.AndroidDomElement;
import org.jetbrains.android.dom.converters.AndroidPackageConverter;
import org.jetbrains.android.dom.converters.AndroidResourceReferenceBase;
import org.jetbrains.android.dom.converters.ConstantFieldConverter;
import org.jetbrains.android.dom.converters.ResourceReferenceConverter;
import org.jetbrains.android.dom.resources.ResourceNameConverter;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.must.android.module.extension.AndroidModuleExtension;

import static com.android.SdkConstants.*;

/**
 * @author Eugene.Kudelevsky
 */
public class AndroidXmlSpellcheckingStrategy extends XmlSpellcheckingStrategy {
  private final MyResourceReferenceTokenizer myResourceReferenceTokenizer = new MyResourceReferenceTokenizer();

  private final Tokenizer<XmlAttributeValue> myAttributeValueRenamingTokenizer = new Tokenizer<XmlAttributeValue>() {
    @Override
    public void tokenize(@NotNull XmlAttributeValue element, TokenConsumer consumer) {
      consumer.consumeToken(element, true, TextSplitter.getInstance());
    }
  };

  @Override
  public boolean isMyContext(@NotNull PsiElement element) {
    // The AndroidXmlSpellCheckingStrategy completely replaces the
    // default XML spell checking strategy (which happens to be
    // its super class, XmlSpellcheckingStrategy) by always returning
    // true. Since it's registered before the default strategy, this means
    // it always wins.
    //
    // There are two reasons we want to replace it:
    // (1) to specially handle resource references; these should not
    //     be typo-checked since they are not up to the user.
    //     (For local declarations, they'll be shown as typos in the
    //     name attribute in the item definition.
    // (2) to skip typo checking completely in files that are not in
    //     English. When you are editing a string in values-nb, the IDE should
    //     not be flagging those words against an English dictionary.
    //
    // Hardcoding this to English is not ideal, but we don't have a way to
    // check which language the dictionary/dictionaries correspond to,
    // and english.dic is included by default.

    return true;
  }

  @NotNull
  @Override
  public Tokenizer getTokenizer(PsiElement element) {
    if (isAttributeValueContext(element)) {
      return getAttributeValueTokenizer(element);
    }

    if (inEnglish(element)) {
      return super.getTokenizer(element);
    }

    return EMPTY_TOKENIZER;
  }

  @NotNull
  public Tokenizer getAttributeValueTokenizer(PsiElement element) {
    assert element instanceof XmlAttributeValue;

    if (AndroidResourceUtil.isIdDeclaration((XmlAttributeValue)element)) {
      return myAttributeValueRenamingTokenizer;
    }
    final PsiElement parent = element.getParent();

    if (parent instanceof XmlAttribute) {
      final String value = ((XmlAttribute)parent).getValue();

      if (value != null) {
        final GenericAttributeValue domValue = DomManager.getDomManager(
          parent.getProject()).getDomElement((XmlAttribute)parent);

        if (domValue != null) {
          final Converter converter = domValue.getConverter();

          if (converter instanceof ResourceReferenceConverter) {
            return myResourceReferenceTokenizer;
          }
          else if (converter instanceof ConstantFieldConverter) {
            return EMPTY_TOKENIZER;
          }
          else if (converter instanceof ResourceNameConverter || converter instanceof AndroidPackageConverter) {
            return myAttributeValueRenamingTokenizer;
          }
        }
      }
    }
    return super.getTokenizer(element);
  }

  private static boolean isAttributeValueContext(@NotNull PsiElement element) {
    if (!(element instanceof XmlAttributeValue)) {
      return false;
    }
    PsiElement parent = element.getParent();
    parent = parent != null ? parent.getParent() : null;

    if (!(parent instanceof XmlTag)) {
      return false;
    }
    final DomElement domElement = DomManager.getDomManager(
      element.getProject()).getDomElement((XmlTag)parent);
    if (domElement instanceof AndroidDomElement) {
      return inEnglish(element);
    }

    return false;
  }

  /**
   * Returns true if the given element is in an XML file that is in an English resource.
   * Manifest files are considered to be in English, as are resources in base folders
   * (unless a locale is explicitly defined on the root element)
   */
  private static boolean inEnglish(PsiElement element) {
    XmlFile file = PsiTreeUtil.getParentOfType(element, XmlFile.class);
    if (file != null) {
      String name = file.getName();
      if (name.equals(ANDROID_MANIFEST_XML)) {
        return true;
      } else if (name.equals("generated.xml")) {
        // Android Studio Workaround for issue https://code.google.com/p/android/issues/detail?id=76715
        // If this a generated file like this:
        //   ${project}/${module}/build/generated/res/generated/{test?}/${flavors}/${build-type}/values/generated.xml
        // ? If so, skip it.
        AndroidModuleExtension facet = ModuleUtilCore.getExtension(file, AndroidModuleExtension.class);
        VirtualFile virtualFile = file.getVirtualFile();
        if (facet != null && facet.isGradleProject() && virtualFile != null) {
          IdeaAndroidProject project = facet.getIdeaAndroidProject();
          if (project != null) {
            VirtualFile buildFolder = VfsUtil.findFileByIoFile(project.getDelegate().getBuildFolder(), false);
            if (buildFolder != null && VfsUtilCore.isAncestor(buildFolder, virtualFile, false)) {
              return false;
            }
          }
        }
      }
      PsiDirectory dir = file.getParent();
      if (dir != null) {
        String locale = LintUtils.getLocaleAndRegion(dir.getName());
        if (locale == null) {
          locale = getToolsLocale(file);
        }
        return locale == null || locale.startsWith("en") || locale.equals("b+en") || locale.startsWith("b+en+");
      }
    }

    return false;
  }

  @Nullable
  private static String getToolsLocale(XmlFile file) {
    // See if the root element specifies a locale to use
    XmlTag rootTag = file.getRootTag();
    if (rootTag != null) {
      return rootTag.getAttributeValue(ATTR_LOCALE, TOOLS_URI);
    }

    return null;
  }

  private static class MyResourceReferenceTokenizer extends XmlAttributeValueTokenizer {
    @Nullable
    private static AndroidResourceReferenceBase findResourceReference(PsiElement element) {
      for (PsiReference reference : element.getReferences()) {
        if (reference instanceof AndroidResourceReferenceBase) {
          return (AndroidResourceReferenceBase)reference;
        }
      }
      return null;
    }

    @Override
    public void tokenize(@NotNull final XmlAttributeValue element, final TokenConsumer consumer) {
      final AndroidResourceReferenceBase reference = findResourceReference(element);

      if (reference != null) {
        if (reference.getResourceValue().getPackage() == null) {
          consumer.consumeToken(element, true, TextSplitter.getInstance());
        }
        return;
      }

      // The super implementation already filters out hex color definitions like #001122, but it's limited to RGB colors, not ARGB.
      if (isColorString(element.getValue())) {
        return;
      }

      super.tokenize(element, consumer);
    }
  }

  private static boolean isColorString(@NotNull String s) {
    int length = s.length();
    // #rgb to #aarrggbb
    if (length < 4 || length > 9) {
      return false;
    }

    int i = 0;
    if (s.charAt(i++) != '#') {
      return false;
    }

    for (; i < length; i++) {
      if (!StringUtil.isHexDigit(s.charAt(i))) {
        return false;
      }
    }
    return true;
  }
}
