/*
 * 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.validation;

import com.intellij.openapi.editor.Document;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.XmlElementVisitor;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtilRt;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NotNull;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.Attributes2Impl;
import org.xml.sax.ext.Locator2Impl;

import java.util.Map;

final class Psi2SaxAdapter extends XmlElementVisitor implements PsiElementProcessor<PsiElement> {
  private final ContentHandler myHandler;

  Psi2SaxAdapter(ContentHandler handler) {
    myHandler = handler;
  }

  @Override
  public void visitXmlElement(@NotNull XmlElement element) {
    if (element instanceof XmlEntityRef) {
      XmlUtil.processXmlElements(element, this, false, true);
    }
    super.visitXmlElement(element);
  }

  @Override
  public void visitXmlToken(@NotNull XmlToken token) {
    if (token.getTokenType() == XmlTokenType.XML_DATA_CHARACTERS) {
      handleText(token, token.getText());
    }
  }

  private void handleText(XmlElement element, String text) {
    try {
      setLocation(element);
      myHandler.characters(text.toCharArray(), 0, text.length());
    } catch (SAXException e) {
      throw new ParseError(e);
    }
  }

  @Override
  public boolean execute(@NotNull PsiElement element) {
    element.accept(this);
    return true;
  }

  @Override
  public void visitXmlDocument(@NotNull XmlDocument document) {
    try {
      myHandler.startDocument();
      final XmlTag rootTag = document.getRootTag();
      if (rootTag != null) {
        rootTag.accept(this);
      }
      myHandler.endDocument();
    } catch (SAXException e) {
      throw new ParseError(e);
    }
  }

  @Override
  public void visitXmlTag(@NotNull XmlTag tag) {
    try {
      setLocation(tag);

      final Map<String,String> map = tag.getLocalNamespaceDeclarations();
      final String[] prefixes = ArrayUtilRt.toStringArray(map.keySet());
      for (String prefix : prefixes) {
        myHandler.startPrefixMapping(prefix, map.get(prefix));
      }

      final Attributes2Impl atts = new Attributes2Impl();
      final XmlAttribute[] xmlAttributes = tag.getAttributes();
      for (XmlAttribute attribute : xmlAttributes) {
        final String s = attribute.getName();
        if (!"xmlns".equals(s) && !s.startsWith("xmlns:")) {
          final String uri = attribute.getNamespace();
          atts.addAttribute(s.contains(":") ? uri : "", attribute.getLocalName(), s, "PCDATA", attribute.getValue());
        }
      }

      final String namespace = tag.getNamespace();
      final String localName = tag.getLocalName();
      final String name = tag.getName();
      myHandler.startElement(namespace, localName, name, atts);

      PsiElement child = tag.getFirstChild();
      while (child != null) {
        child.accept(this);
        child = child.getNextSibling();
      }

      myHandler.endElement(namespace, localName, name);

      for (int i = prefixes.length - 1; i >= 0; i--) {
        String prefix = prefixes[i];
        myHandler.endPrefixMapping(prefix);
      }
    } catch (SAXException e) {
      throw new ParseError(e);
    }
  }

  @Override
  public void visitXmlText(@NotNull XmlText text) {
    handleText(text, text.getValue());
  }

  private void setLocation(PsiElement text) {
    final PsiFile psiFile = text.getContainingFile();
    final Document document = PsiDocumentManager.getInstance(text.getProject()).getDocument(psiFile);
    if (document == null) {
      return;
    }
    final VirtualFile virtualFile = psiFile.getVirtualFile();
    if (virtualFile == null) {
      return;
    }

    final Locator2Impl locator = new Locator2Impl();
    locator.setSystemId(VfsUtilCore.fixIDEAUrl(virtualFile.getUrl()));

    final int offset = text.getTextRange().getEndOffset();
    final int lineNumber = document.getLineNumber(offset);

    locator.setLineNumber(lineNumber + 1);
    locator.setColumnNumber(1 + offset - document.getLineStartOffset(lineNumber));

    myHandler.setDocumentLocator(locator);
  }

  public static class ParseError extends RuntimeException {
    public ParseError(SAXException e) {
      super(e);
    }
  }
}
