/*
 * Copyright 2013 Red Hat, Inc.
 *
 *    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.jboss.errai.idea.plugin.ui;

import static com.intellij.psi.search.GlobalSearchScope.projectScope;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.util.PsiUtil;
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 org.jboss.errai.idea.plugin.ui.model.ConsolidateDataFieldElementResult;
import org.jboss.errai.idea.plugin.ui.model.DataFieldCacheHolder;
import org.jboss.errai.idea.plugin.ui.model.TemplateExpression;
import org.jboss.errai.idea.plugin.ui.model.TemplateMetaData;
import org.jboss.errai.idea.plugin.util.AnnotationSearchResult;
import org.jboss.errai.idea.plugin.util.AnnotationValueElement;
import org.jboss.errai.idea.plugin.util.CacheProvider;
import org.jboss.errai.idea.plugin.util.Types;
import org.jboss.errai.idea.plugin.util.Util;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Mike Brock
 */
public class TemplateUtil {
  public static final String DATA_FIELD_TAG_ATTRIBUTE = "data-field";
  public static final String ID_ATTRIBUTE = "id";

  private static final Key<DataFieldCacheHolder> dataFieldsCacheKey = Key.create("dataFieldsCache");

  public static DataFieldExistence dataFieldExistenceCheck(PsiAnnotation annotation, TemplateMetaData metaData) {
    final Multimap<String, TemplateDataField> inScopeDataFields = metaData.getAllDataFieldsInTemplate(false);
    final Map<String, ConsolidateDataFieldElementResult> dataFields = metaData.getConsolidatedDataFields();

    final AnnotationValueElement annoValueEl = Util.getValueStringFromAnnotationWithDefault(annotation);
    final String annoValue = annoValueEl.getValue();

    final Collection<TemplateDataField> result = inScopeDataFields.get(annoValue);
    if (result.isEmpty()) {
      if (dataFields.containsKey(annoValue)) {
        return DataFieldExistence.OUT_OF_SCOPE;
      }
      else {
        return DataFieldExistence.DOES_NOT_EXIST;
      }
    }
    else {
      return DataFieldExistence.EXISTS;
    }
  }

  public static enum DataFieldExistence {
    EXISTS, DOES_NOT_EXIST, OUT_OF_SCOPE;
  }

  public static class Ownership {
    private final Set<PsiClass> templateClasses;

    public Ownership(Set<PsiClass> templateClasses) {
      this.templateClasses = templateClasses;
    }

    public Set<PsiClass> getTemplateClasses() {
      return templateClasses;
    }

    public boolean isValid() {
      return true;
    }
  }

  public static TemplateExpression parseReference(String referenceString) {
    int nodeSpecifier = referenceString.indexOf('#');

    final String fileName;
    final String rootNode;
    if (nodeSpecifier == -1) {
      fileName = referenceString;
      rootNode = "";
    }
    else {
      fileName = referenceString.substring(0, nodeSpecifier);
      rootNode = referenceString.substring(nodeSpecifier + 1);
    }

    return new TemplateExpression(fileName.trim(), rootNode.trim());
  }

  /**
   * Finds all "data-field" tags for the specified {@link org.jboss.errai.idea.plugin.ui.model.TemplateMetaData}.
   *
   * @param templateMetaData
   *     the {@link org.jboss.errai.idea.plugin.ui.model.TemplateMetaData} to use to find the tag.
   * @param project
   *     the IntelliJ <tt>Project</tt> reference.
   * @param includeRoot
   *     boolean indicating whether or not to consider the root node in the search. If set to <tt>false</tt>,
   *     only children of the node are considered. If Set to <tt>true</tt>, the root node itself is checked
   *     to see if it is a data-field.
   *
   * @return Map of all datafields in the template class.
   */
  @NotNull
  public static Multimap<String, TemplateDataField> findAllDataFieldTags(TemplateMetaData templateMetaData,
                                                                         Project project,
                                                                         boolean includeRoot) {
    if (!templateMetaData.getTemplateExpression().hasRootNode()) {
      includeRoot = true;
    }

    VirtualFile vf = templateMetaData.getTemplateFile();
    XmlTag rootTag = templateMetaData.getRootTag();
    if (vf == null) {
      return ImmutableMultimap.of();
    }

    final PsiManager instance = PsiManager.getInstance(project);
    final PsiFile file = instance.findFile(vf);

    if (file == null) {
      return ImmutableMultimap.of();
    }

    final XmlFile xmlFile = (XmlFile) file;
    if (rootTag == null) {
      rootTag = xmlFile.getRootTag();
    }

    return findAllDataFieldTags(file, rootTag, includeRoot);
  }

  private static Multimap<String, TemplateDataField> findAllDataFieldTags(VirtualFile vf,
                                                                          XmlTag rootTag,
                                                                          Project project,
                                                                          boolean includeRoot) {
    if (vf == null) {
      return ImmutableMultimap.of();
    }

    final PsiManager instance = PsiManager.getInstance(project);
    final PsiFile file = instance.findFile(vf);

    if (file == null) {
      return ImmutableMultimap.of();

    }

    if (rootTag == null) {
      rootTag = ((XmlFile) file).getRootTag();
    }
    else {
    }

    return findAllDataFieldTags(file, rootTag, includeRoot);
  }

  @NotNull
  public static Multimap<String, TemplateDataField> findAllDataFieldTags(final PsiFile templateFile,
                                                                         final XmlTag rootTag,
                                                                         final boolean includeRoot) {
    final Multimap<String, TemplateDataField> value
        = Util.getOrCreateCache(dataFieldsCacheKey, templateFile, new CacheProvider<DataFieldCacheHolder>() {
      @Override
      public DataFieldCacheHolder provide() {
        final Multimap<String, TemplateDataField> allDataFieldTags = findAllDataFieldTags(rootTag, includeRoot);
        return new DataFieldCacheHolder(templateFile.getModificationStamp(), allDataFieldTags);
      }

      @Override
      public boolean isCacheValid(DataFieldCacheHolder dataFieldCacheHolder) {
        return dataFieldCacheHolder.getTime() == templateFile.getModificationStamp();
      }
    }).getValue();

    final Multimap<String, TemplateDataField> templateDataFields = HashMultimap.create(value);
    Iterator<TemplateDataField> iterator = templateDataFields.values().iterator();
    final PsiElement rootElement = rootTag.getOriginalElement();

    while (iterator.hasNext()) {
      TemplateDataField field = iterator.next();
      final PsiElement originalElement = field.getTag().getOriginalElement();

      if (!includeRoot && !Util.isChild(originalElement, rootElement)) {
        iterator.remove();
      }

    }
    return templateDataFields;
  }

  private static Multimap<String, TemplateDataField> findAllDataFieldTags(XmlTag rootTag, boolean includeRoot) {
    Multimap<String, TemplateDataField> references = HashMultimap.create();
    if (rootTag == null) {
      return references;
    }

    if (includeRoot
        && rootTag.getAttribute(DATA_FIELD_TAG_ATTRIBUTE) != null || rootTag.getAttribute(ID_ATTRIBUTE) != null) {
      final XmlAttribute attribute;
      if (rootTag.getAttribute(DATA_FIELD_TAG_ATTRIBUTE) != null) {
        attribute = rootTag.getAttribute(DATA_FIELD_TAG_ATTRIBUTE);
      } else {
        attribute = rootTag.getAttribute(ID_ATTRIBUTE);
      }

      if (attribute == null) {
        return references;
      }

      final String value = attribute.getValue();
      references.put(value, new TemplateDataField(rootTag, value));
    }
    _findDataFieldTags(references, rootTag);
    return references;
  }

  private static void _findDataFieldTags(Multimap<String, TemplateDataField> foundTags, XmlTag root) {
    PsiElement n = root;
    do {
      if (!(n instanceof XmlTag)) {
        continue;
      }
      _scanSubTags(foundTags, (XmlTag) n);
    }
    while ((n = n.getNextSibling()) != null);
  }

  private static void _scanSubTags(Multimap<String, TemplateDataField> foundTags, XmlTag root) {
    _scanTag(foundTags, root);
    for (XmlTag xmlTag : root.getSubTags()) {
      _scanSubTags(foundTags, xmlTag);
    }
  }

  private static void _scanTag(Multimap<String, TemplateDataField> foundTags, XmlTag xmlTag) {
    _scanTag(foundTags, xmlTag, DATA_FIELD_TAG_ATTRIBUTE);
    _scanTag(foundTags, xmlTag, ID_ATTRIBUTE);
  }

  private static void _scanTag(Multimap<String, TemplateDataField> foundTags, XmlTag xmlTag, String dataFieldTagAttribute) {
    XmlAttribute xmlAttribute = xmlTag.getAttribute(dataFieldTagAttribute);
    if (xmlAttribute != null) {
      foundTags.put(xmlAttribute.getValue(), new TemplateDataField(xmlTag, xmlAttribute.getValue()));
    }
  }

  public static PsiAnnotation findTemplatedAnnotation(PsiElement element) {
    final PsiClass topLevelClass;

    if (element.getParent() == null) {
      if (element instanceof PsiClass) {
        topLevelClass = (PsiClass) element;
      }
      else {
        return null;
      }
    }
    else {
      topLevelClass = PsiUtil.getTopLevelClass(element);
    }

    if (topLevelClass == null) {
      return null;
    }

    final PsiModifierList modifierList = topLevelClass.getModifierList();

    if (modifierList == null) {
      return null;
    }

    final PsiAnnotation[] annotations = modifierList.getAnnotations();
    for (PsiAnnotation psiAnnotation : annotations) {
      final String qualifiedName = psiAnnotation.getQualifiedName();
      if (qualifiedName == null) {
        continue;
      }
      if (qualifiedName.equals(Types.TEMPLATED)) {
        return psiAnnotation;
      }
    }
    return null;
  }

  public static TemplateMetaData getTemplateMetaData(PsiElement element) {
    return getTemplateMetaData(findTemplatedAnnotation(element), element.getProject());
  }

  private static TemplateMetaData getTemplateMetaData(PsiAnnotation annotation, Project project) {
    if (annotation == null) return null;

    final String qualifiedName = annotation.getQualifiedName();

    if (qualifiedName == null) return null;

    if (!qualifiedName.equals(Types.TEMPLATED)) {
      annotation = findTemplatedAnnotation(annotation);
      if (annotation == null) return null;
    }

    final PsiClass templateClass = PsiUtil.getTopLevelClass(annotation);

    if (templateClass == null) {
      return null;
    }

    final PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes();

    final String templateName;
    if (attributes.length == 0) {
      templateName = templateClass.getName() + ".html";
    }
    else {
      if (!(attributes[0].getValue() instanceof PsiLiteralExpression)) {
        return null;
      }

      final PsiLiteralExpression literalExpression = (PsiLiteralExpression) attributes[0].getValue();
      if (literalExpression == null) {
        return null;
      }

      String text = literalExpression.getText().replace(Util.INTELLIJ_MAGIC_STRING, "");
      templateName = text.substring(1, text.length() - 1);
    }

    final PsiFile containingFile = templateClass.getContainingFile().getOriginalFile();
    PsiDirectory containerDir = containingFile.getParent();

    if (containerDir == null) {
      return null;
    }

    final TemplateExpression reference = TemplateUtil.parseReference(templateName);

    final String fileName;
    if ("".equals(reference.getFileName())) {
      fileName = templateClass.getName() + ".html";
    }
    else {
      fileName = reference.getFileName();
    }

    final VirtualFile virtualFile = containerDir.getVirtualFile();
    VirtualFile fileByRelativePath = virtualFile.findFileByRelativePath(fileName);
    if (fileByRelativePath != null && fileByRelativePath.isDirectory()) {
      fileByRelativePath = null;
    }

    // if we didn't find the file in the current container,
    // and this is a maven project, it might located in the resources folder
    if (fileByRelativePath == null) {
      // see if this is a maven project  check for a pom.xml in the root folder
      VirtualFile vProjectDir = project.getBaseDir();
      VirtualFile vPom = vProjectDir.findChild("pom.xml");
      if (vPom != null) {
        // look in the src/main/resources folder for the corresponding html file
        String containerPath = virtualFile.getPath();
        String resourcePath = containerPath.replaceAll("src/main/java", "src/main/resources");
        File resourceFile = new File(resourcePath, fileName);
        VirtualFile vf = LocalFileSystem.getInstance().findFileByIoFile(resourceFile);
        if (vf != null) {
          fileByRelativePath = vf;
        }
      }
    }


    final XmlTag rootTag;
    if (fileByRelativePath == null) {
      rootTag = null;
    }
    else if (reference.getRootNode().equals("")) {
      final PsiFile file = PsiManager.getInstance(project).findFile(fileByRelativePath);
      if (file != null) {
        rootTag = ((XmlFile) file).getRootTag();
      }
      else {
        rootTag = null;
      }
    }
    else {
      Multimap<String, TemplateDataField> allDataFieldTags
          = findAllDataFieldTags(fileByRelativePath, null, project, true);

      final Collection<TemplateDataField> dataFieldReference = allDataFieldTags.get(reference.getRootNode());
      // if both data-field and id are the same, dataFieldReference will have
      // two reference to the same element. So, the root tag is valid if we have
      // any values in the iterator
      Iterator<TemplateDataField> dataFieldIterator = dataFieldReference.iterator();
      if (dataFieldIterator.hasNext()) {
        rootTag = dataFieldIterator.next().getTag();
      }
      else {
        rootTag = null;
      }
    }

    return new TemplateMetaData(reference,
        attributes.length == 0,
        attributes.length == 0 ? null : attributes[0],
        templateClass,
        fileByRelativePath,
        rootTag,
        project);
  }

  public static Collection<String> extractDataFieldList(Collection<AnnotationSearchResult> dataFieldElements) {
    final List<String> elements = new ArrayList<String>();
    for (AnnotationSearchResult element : dataFieldElements) {
      elements.add(Util.getValueStringFromAnnotationWithDefault(element.getAnnotation()).getValue());
    }
    return elements;
  }

  public static Map<String, ConsolidateDataFieldElementResult> getConsolidatedDataFields(PsiElement element, Project project) {
    final TemplateMetaData metaData = TemplateUtil.getTemplateMetaData(element);
    if (metaData == null) {
      return Collections.emptyMap();
    }

    final PsiClass topLevelClass = PsiUtil.getTopLevelClass(element);
    if (topLevelClass == null) {
      return Collections.emptyMap();
    }

    final String beanClass = topLevelClass.getQualifiedName();

    final Map<String, ConsolidateDataFieldElementResult> results = new LinkedHashMap<String, ConsolidateDataFieldElementResult>();

    final Collection<AnnotationSearchResult> allInjectionPoints
        = Util.findAllAnnotatedElements(element, Types.DATAFIELD);

    for (AnnotationSearchResult r : allInjectionPoints) {
      final String value = Util.getValueStringFromAnnotationWithDefault(r.getAnnotation()).getValue();
      results.put(value, new ConsolidateDataFieldElementResult(value, beanClass, r.getOwningElement(), true));
    }

    final Multimap<String, TemplateDataField> allDataFieldTags = TemplateUtil.findAllDataFieldTags(metaData, project, false);
    for (TemplateDataField ref : allDataFieldTags.values()) {
      final XmlAttribute attribute = ref.getTag().getAttribute("data-field");
      if (attribute == null) {
        continue;
      }

      final XmlAttributeValue valueElement = attribute.getValueElement();
      if (results.containsKey(ref.getDataFieldName())) {
        results.get(ref.getDataFieldName()).setLinkingElement(valueElement);
        continue;
      }

      results.put(ref.getDataFieldName(), new ConsolidateDataFieldElementResult(ref.getDataFieldName(),
          metaData.getTemplateExpression().getFileName(), valueElement, false));
    }

    return results;
  }

  public static Collection<TemplateMetaData> getTemplateOwners(final PsiFile file) {
    final List<TemplateMetaData> templateOwners = new ArrayList<TemplateMetaData>();
    final PsiClass psiClass = JavaPsiFacade.getInstance(
        file.getProject()).findClass(Types.GWT_COMPOSITE,
        GlobalSearchScope.allScope(file.getProject())
    );

    if (psiClass == null) {
      return Collections.emptyList();
    }

    for (PsiClass c : ClassInheritorsSearch.search(psiClass, projectScope(file.getProject()), true)) {
      final TemplateMetaData templateMetaData = TemplateUtil.getTemplateMetaData(c);
      if (templateMetaData == null) {
        continue;
      }
      final VirtualFile vTemplateFile = templateMetaData.getTemplateFile();
      if (vTemplateFile == null) {
        continue;
      }
      final String templateFile = vTemplateFile.getCanonicalPath();
      final VirtualFile virtualFile = file.getOriginalFile().getVirtualFile();
      if (virtualFile == null || templateFile == null) {
        continue;
      }
      if (templateFile.equals(virtualFile.getCanonicalPath())) {
        templateOwners.add(templateMetaData);
      }
    }

    return templateOwners;
  }

  public static PsiFile getFileFromElement(PsiElement element) {
    do {
      if (element instanceof PsiFile) {
        return (PsiFile) element;
      }
    }
    while ((element = element.getParent()) != null);
    return null;
  }
}
