/*
 * Copyright 2007 Sascha Weinreuter
 *
 * 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.intellij.plugins.relaxNG.model.descriptors;

import com.intellij.openapi.util.Comparing;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.impl.BasicXmlAttributeDescriptor;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.kohsuke.rngom.digested.DAttributePattern;
import org.xml.sax.Locator;

import javax.xml.namespace.QName;
import java.util.*;

public class RngXmlAttributeDescriptor extends BasicXmlAttributeDescriptor {
  @NonNls
  private static final QName UNKNOWN = new QName("", "#unknown");

  private static final TObjectHashingStrategy<Locator> HASHING_STRATEGY = new TObjectHashingStrategy<Locator>() {
    public int computeHashCode(Locator o) {
      final String s = o.getSystemId();
      return o.getLineNumber() * 31 + o.getColumnNumber() * 23 + (s != null ? s.hashCode() * 11 : 0);
    }
    public boolean equals(Locator o, Locator o1) {
      if ((o.getLineNumber() == o1.getLineNumber() && o.getColumnNumber() == o1.getColumnNumber())) {
        if (Comparing.equal(o.getSystemId(), o1.getSystemId())) {
          return true;
        }
      }
      return false;
    }
  };

  private final Map<String, String> myValues;
  private final boolean myOptional;
  private final RngElementDescriptor myElementDescriptor;
  private final THashSet<Locator> myDeclarations = new THashSet<Locator>(HASHING_STRATEGY);
  private final QName myName;

  RngXmlAttributeDescriptor(RngElementDescriptor elementDescriptor, DAttributePattern pattern, Map<String, String> values, boolean optional) {
    this(elementDescriptor, getName(pattern), values, optional, pattern.getLocation());
  }

  private static QName getName(DAttributePattern pattern) {
    final Iterator<QName> iterator = pattern.getName().listNames().iterator();
    return iterator.hasNext() ? iterator.next() : UNKNOWN;
  }

  private RngXmlAttributeDescriptor(RngElementDescriptor elementDescriptor, QName name, Map<String, String> values, boolean optional, Locator... locations) {
    myElementDescriptor = elementDescriptor;
    myValues = values;
    myOptional = optional;
    myName = name;
    myDeclarations.addAll(Arrays.asList(locations));
  }

  public RngXmlAttributeDescriptor mergeWith(RngXmlAttributeDescriptor d) {
    final QName name = d.myName.equals(UNKNOWN) ? myName : d.myName;

    final HashMap<String, String> values = new HashMap<String, String>(myValues);
    values.putAll(d.myValues);

    final THashSet<Locator> locations = new THashSet<Locator>(myDeclarations, HASHING_STRATEGY);
    locations.addAll(d.myDeclarations);

    return new RngXmlAttributeDescriptor(myElementDescriptor, name, values, myOptional || d.myOptional, locations.toArray(new Locator[locations.size()]));
  }

  public boolean isRequired() {
    return !myOptional;
  }

  public boolean isFixed() {
    return isEnumerated() && myValues.size() == 1;
  }

  public boolean hasIdType() {
    return myValues.values().contains("ID");
  }

  public boolean hasIdRefType() {
    return myValues.values().contains("IDREF");
  }

  @Nullable
  public String getDefaultValue() {
    return isEnumerated() ? myValues.keySet().iterator().next() : null;
  }

  public boolean isEnumerated() {
    return myValues.size() > 0 && myValues.get(null) == null;
  }

  public String[] getEnumeratedValues() {
    if (myValues.size() > 0) {
      final Map<String, String> copy;
      if (myValues.get(null) != null) {
        copy = new HashMap<String, String>(myValues);
        copy.remove(null);
      } else {
        copy = myValues;
      }
      return copy.keySet().toArray(new String[copy.size()]);
    } else {
      return ArrayUtil.EMPTY_STRING_ARRAY;
    }
  }

  public PsiElement getDeclaration() {
    final Iterator<Locator> it = myDeclarations.iterator();
    if (!it.hasNext()) return null;

    return myElementDescriptor.getDeclaration(it.next());
  }

  public Collection<PsiElement> getDeclarations() {
    return ContainerUtil.map2List(myDeclarations, new Function<Locator, PsiElement>() {
      public PsiElement fun(Locator locator) {
        return myElementDescriptor.getDeclaration(locator);
      }
    });
  }
  
  @Override
  public String getName(PsiElement context) {
    final XmlTag tag = PsiTreeUtil.getParentOfType(context, XmlTag.class, false, PsiFile.class);
    if (tag != null) {
      final String uri = myName.getNamespaceURI();
      final String prefix = tag.getPrefixByNamespace(uri);
      if (prefix != null) {
        if (prefix.length() == 0) {
          return myName.getLocalPart();
        } else {
          return prefix + ":" + myName.getLocalPart();
        }
      }
    }
    if (myName.getNamespaceURI().length() > 0) {
      final String prefix2 = myName.getPrefix();
      if (prefix2 != null && prefix2.length() > 0) {
        return prefix2 + ":" + myName.getLocalPart();
      }
    }
    return myName.getLocalPart();
  }

  @NonNls
  public String getName() {
    return myName.getLocalPart();
  }

  public void init(PsiElement element) {

  }

  public Object[] getDependences() {
    return myElementDescriptor.getDependences();
  }

  @Override
  public String validateValue(XmlElement context, String value) {
    if (isTokenDatatype(value)) {
      value = normalizeSpace(value);
    }
    return super.validateValue(context, value);
  }

  private boolean isTokenDatatype(String value) {
    if (myValues.containsKey(value)) {
      return "token".equals(myValues.get(value));
    }

    value = normalizeSpace(value);
    return myValues.containsKey(value) && "token".equals(myValues.get(value));
  }

  private static String normalizeSpace(String value) {
    return value.replaceAll("\\s+", " ").trim();
  }
}