package com.thaiopensource.resolver.xml.sax;

import com.thaiopensource.resolver.AbstractResolver;
import com.thaiopensource.resolver.BasicResolver;
import com.thaiopensource.resolver.Identifier;
import com.thaiopensource.resolver.Input;
import com.thaiopensource.resolver.Resolver;
import com.thaiopensource.resolver.ResolverException;
import com.thaiopensource.resolver.xml.ExternalDTDSubsetIdentifier;
import com.thaiopensource.resolver.xml.ExternalEntityIdentifier;
import com.thaiopensource.resolver.xml.ExternalIdentifier;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.EntityResolver2;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;

/**
 *
 */
public class SAX {
  private SAX() {
  }

  private static final class EntityResolverWrapper extends AbstractResolver {
    private final EntityResolver entityResolver;
    private final EntityResolver2 entityResolver2;
    private final boolean promiscuous;

    private EntityResolverWrapper(EntityResolver entityResolver, boolean promiscuous) {
      this.entityResolver = entityResolver;
      if (entityResolver instanceof EntityResolver2)
        entityResolver2 = (EntityResolver2)entityResolver;
      else
        entityResolver2 = null;
      this.promiscuous = promiscuous;
    }

    public void open(Input input) throws IOException, ResolverException {
      if (!input.isUriDefinitive())
        return;
      URI uri;
      try {
        uri = new URI(input.getUri());
      }
      catch (URISyntaxException e) {
        throw new ResolverException(e);
      }
      if (!uri.isAbsolute())
        throw new ResolverException("cannot open relative URI: " + uri);
      URL url = new URL(uri.toASCIIString());
      // XXX should set the encoding properly
      // XXX if this is HTTP and we've been redirected, should do input.setURI with the new URI
      input.setByteStream(url.openStream());
    }
    
    public void resolve(Identifier id, Input input) throws IOException, ResolverException {
      if (input.isResolved())
        return;
      String publicId;
      String entityName = null;
      if (id instanceof ExternalIdentifier) {
        publicId = ((ExternalIdentifier)id).getPublicId();
        if (id instanceof ExternalEntityIdentifier)
          entityName = ((ExternalEntityIdentifier)id).getEntityName();
        else if (id instanceof ExternalDTDSubsetIdentifier)
          entityName = "[dtd]";
      }
      else {
        if (!promiscuous)
          return;
        publicId = null;
      }
      try {
        InputSource inputSource;
        if (entityName != null && entityResolver2 != null)
          inputSource = entityResolver2.resolveEntity(entityName,
                                                      publicId,
                                                      id.getBase(),
                                                      id.getUriReference());
        else
          inputSource = entityResolver.resolveEntity(publicId, getSystemId(id));
        if (inputSource != null)
          setInput(input, inputSource);
      }
      catch (SAXException e) {
        throw toResolverException(e);
      }
    }
  }

  public static Resolver createResolver(final EntityResolver entityResolver, boolean promiscuous) {
    return new EntityResolverWrapper(entityResolver, promiscuous);
  }

  public static EntityResolver2 createEntityResolver(Resolver resolver) {
    if (resolver == null)
      throw new NullPointerException();
    return new EntityResolverImpl(resolver);
  }

  public static Input createInput(InputSource inputSource) {
    Input input = new Input();
    setInput(input, inputSource);
    return input;
  }
  // public because needed by transform package
  public static void setInput(Input input, InputSource inputSource) {
    input.setByteStream(inputSource.getByteStream());
    input.setCharacterStream(inputSource.getCharacterStream());
    input.setUri(inputSource.getSystemId());
    input.setEncoding(inputSource.getEncoding());
  }

  public static Exception getWrappedException(SAXException e) {
    // not purely a wrapper
    if (e.getMessage() != null)
      return null;
    return e.getException();
  }

  public static ResolverException toResolverException(SAXException e) {
    Exception wrapped = getWrappedException(e);
    if (wrapped != null) {
      if (wrapped instanceof ResolverException)
        return (ResolverException)wrapped;
      return new ResolverException(wrapped);
    }
    return new ResolverException(e);
  }
  
  public static SAXException toSAXException(ResolverException e) {
    Throwable cause = e.getCause();
    if (cause != null && cause instanceof SAXException)
      return (SAXException)cause;
    return new SAXException(e);
  }

  static InputSource createInputSource(Input input) {
    InputSource inputSource = new InputSource();
    inputSource.setByteStream(input.getByteStream());
    inputSource.setCharacterStream(input.getCharacterStream());
    inputSource.setEncoding(input.getEncoding());
    inputSource.setSystemId(input.getUri());
    return inputSource;
  }

  static String getSystemId(Identifier id) {
    try {
      return BasicResolver.resolveUri(id);
    }
    catch (ResolverException e) { }
    return id.getUriReference();
  }

  // precondition: input.isResolved()
  static InputSource createInputSource(Identifier id, Input input) {
    InputSource inputSource = createInputSource(input);
    if (id instanceof ExternalIdentifier)
      inputSource.setPublicId(((ExternalIdentifier)id).getPublicId());
    if (inputSource.getSystemId() == null)
      inputSource.setSystemId(getSystemId(id));
    return inputSource;
  }

  static private class EntityResolverImpl implements EntityResolver2 {
    private final Resolver resolver;

    private EntityResolverImpl(Resolver resolver) {
      this.resolver = resolver;
    }

    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
      if (systemId == null)
        return null;
      ExternalIdentifier id = new ExternalIdentifier(systemId, null, publicId);
      Input input = new Input();
      try {
        resolver.resolve(id, input);
      }
      catch (ResolverException e) {
        throw toSAXException(e);
      }
      if (input.isResolved())
        return createInputSource(id, input);
      return null;
    }

    public InputSource resolveEntity(String name, String publicId, String base, String systemId) throws SAXException, IOException {
      if (systemId == null)
        return null;
      ExternalIdentifier id;
      if ("[doc]".equals(name))
        id = new ExternalDTDSubsetIdentifier(systemId, base, publicId, null);
      else if (name == null || name.indexOf('[') >= 0 || name.indexOf('#') >= 0)
        id = new ExternalIdentifier(systemId, base, publicId);
      else
        id = new ExternalEntityIdentifier(systemId, base, publicId, name);
      Input input = new Input();
      try {
        resolver.resolve(id, input);
      }
      catch (ResolverException e) {
        throw toSAXException(e);
      }
      if (input.isResolved())
        return createInputSource(id, input);
      return null;
    }

    public InputSource getExternalSubset(String name, String base) throws SAXException, IOException {
      return null;
    }
  }
}
