package nokogiri;

import static nokogiri.internals.NokogiriHelpers.adjustSystemIdIfNecessary;
import static nokogiri.internals.NokogiriHelpers.getNokogiriClass;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;

import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import nokogiri.internals.IgnoreSchemaErrorsErrorHandler;
import nokogiri.internals.SchemaErrorHandler;
import nokogiri.internals.XmlDomParserContext;
import nokogiri.internals.ParserContext;
import nokogiri.internals.ParserContext.Options;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.Helpers;
import org.w3c.dom.Document;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Class for Nokogiri::XML::Schema
 *
 * @author sergio
 * @author Yoko Harada <yokolet@gmail.com>
 */
@JRubyClass(name = "Nokogiri::XML::Schema")
public class XmlSchema extends RubyObject
{
  private static final long serialVersionUID = 1L;

  private Validator validator;

  public
  XmlSchema(Ruby ruby, RubyClass klazz)
  {
    super(ruby, klazz);
  }

  /**
   * Create and return a copy of this object.
   *
   * @return a clone of this object
   */
  @Override
  public Object
  clone() throws CloneNotSupportedException
  {
    return super.clone();
  }

  private Schema
  getSchema(Source source,
            String currentDir,
            String scriptFileName,
            SchemaErrorHandler errorHandler,
            long parseOptions) throws SAXException
  {
    boolean noNet = new ParserContext.Options(parseOptions).noNet;

    SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    SchemaResourceResolver resourceResolver =
      new SchemaResourceResolver(currentDir, scriptFileName, null, errorHandler, noNet);

    schemaFactory.setResourceResolver(resourceResolver);
    schemaFactory.setErrorHandler(errorHandler);

    return schemaFactory.newSchema(source);
  }

  private void
  setValidator(Validator validator)
  {
    this.validator = validator;
  }

  static XmlSchema
  createSchemaInstance(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions)
  {
    Ruby runtime = context.getRuntime();
    XmlSchema xmlSchema = (XmlSchema) NokogiriService.XML_SCHEMA_ALLOCATOR.allocate(runtime, klazz);

    if (parseOptions == null) {
      parseOptions = defaultParseOptions(context.getRuntime());
    }
    long intParseOptions = RubyFixnum.fix2long(Helpers.invoke(context, parseOptions, "to_i"));

    xmlSchema.setInstanceVariable("@errors", runtime.newEmptyArray());
    xmlSchema.setInstanceVariable("@parse_options", parseOptions);

    try {
      SchemaErrorHandler errorHandler =
        new SchemaErrorHandler(context.getRuntime(), (RubyArray)xmlSchema.getInstanceVariable("@errors"));
      Schema schema =
        xmlSchema.getSchema(source,
                            context.getRuntime().getCurrentDirectory(),
                            context.getRuntime().getInstanceConfig().getScriptFileName(),
                            errorHandler,
                            intParseOptions);
      xmlSchema.setValidator(schema.newValidator());
      return xmlSchema;
    } catch (SAXException ex) {
      throw context.getRuntime().newRuntimeError("Could not parse document: " + ex.getMessage());
    }
  }

  protected static IRubyObject
  defaultParseOptions(Ruby runtime)
  {
    return ((RubyClass)runtime.getClassFromPath("Nokogiri::XML::ParseOptions")).getConstant("DEFAULT_SCHEMA");
  }

  /*
   * call-seq:
   *  from_document(doc)
   *
   * Create a new Schema from the Nokogiri::XML::Document +doc+
   */
  @JRubyMethod(meta = true, required = 1, optional = 1)
  public static IRubyObject
  from_document(ThreadContext context, IRubyObject klazz, IRubyObject[] args)
  {
    IRubyObject rbDocument = args[0];
    IRubyObject parseOptions = null;
    if (args.length > 1) {
      parseOptions = args[1];
    }

    if (!(rbDocument instanceof XmlNode)) {
      String msg = "expected parameter to be a Nokogiri::XML::Document, received " + rbDocument.getMetaClass();
      throw context.runtime.newTypeError(msg);
    }
    if (!(rbDocument instanceof XmlDocument)) {
      context.runtime.getWarnings().warn("Passing a Node as the first parameter to Schema.from_document is deprecated. Please pass a Document instead. This will become an error in Nokogiri v1.17.0."); // TODO: deprecated in v1.15.3, remove in v1.17.0
    }

    XmlDocument doc = ((XmlDocument)((XmlNode) rbDocument).document(context));

    RubyArray<?> errors = (RubyArray) doc.getInstanceVariable("@errors");
    if (!errors.isEmpty()) {
      throw((XmlSyntaxError) errors.first()).toThrowable();
    }

    DOMSource source = new DOMSource(doc.getDocument());

    IRubyObject uri = doc.url(context);

    if (!uri.isNil()) {
      source.setSystemId(uri.convertToString().asJavaString());
    }

    return getSchema(context, (RubyClass)klazz, source, parseOptions);
  }

  private static IRubyObject
  getSchema(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions)
  {
    String moduleName = klazz.getName();
    if ("Nokogiri::XML::Schema".equals(moduleName)) {
      return XmlSchema.createSchemaInstance(context, klazz, source, parseOptions);
    } else if ("Nokogiri::XML::RelaxNG".equals(moduleName)) {
      return XmlRelaxng.createSchemaInstance(context, klazz, source, parseOptions);
    }
    return context.getRuntime().getNil();
  }

  @JRubyMethod(visibility = Visibility.PRIVATE)
  public IRubyObject
  validate_document(ThreadContext context, IRubyObject document)
  {
    return validate_document_or_file(context, (XmlDocument)document);
  }

  @JRubyMethod(visibility = Visibility.PRIVATE)
  public IRubyObject
  validate_file(ThreadContext context, IRubyObject file)
  {
    Ruby runtime = context.runtime;

    XmlDomParserContext ctx = new XmlDomParserContext(runtime, RubyFixnum.newFixnum(runtime, 1L));
    ctx.setInputSourceFile(context, file);
    try {
      XmlDocument xmlDocument = ctx.parse(context, getNokogiriClass(runtime, "Nokogiri::XML::Document"), context.nil);
      return validate_document_or_file(context, xmlDocument);
    } catch (Exception ex) {
      RubyArray errors = (RubyArray)context.runtime.newEmptyArray();
      XmlSyntaxError xmlSyntaxError = XmlSyntaxError.createXMLSyntaxError(context.runtime);
      xmlSyntaxError.setException(ex);
      errors.append(xmlSyntaxError);
      return errors;
    }
  }

  IRubyObject
  validate_document_or_file(ThreadContext context, XmlDocument xmlDocument)
  {
    RubyArray errors = context.runtime.newEmptyArray();
    ErrorHandler errorHandler = new SchemaErrorHandler(context.runtime, errors);
    setErrorHandler(errorHandler);

    try {
      validate(xmlDocument.getDocument());
    } catch (SAXException ex) {
      XmlSyntaxError xmlSyntaxError = XmlSyntaxError.createXMLSyntaxError(context.runtime);
      xmlSyntaxError.setException(ex);
      errors.append(xmlSyntaxError);
    } catch (IOException ex) {
      throw context.runtime.newIOError(ex.getMessage());
    }

    return errors;
  }

  protected void
  setErrorHandler(ErrorHandler errorHandler)
  {
    validator.setErrorHandler(errorHandler);
  }

  protected void
  validate(Document document) throws SAXException, IOException
  {
    if (document.getDocumentElement() == null) {
      throw new SAXException("Document is empty");
    }

    DOMSource docSource = new DOMSource(document);
    validator.validate(docSource);
  }

  private class SchemaResourceResolver implements LSResourceResolver
  {
    SchemaLSInput lsInput = new SchemaLSInput();
    String currentDir;
    String scriptFileName;
    SchemaErrorHandler errorHandler;
    boolean noNet;
    //String defaultURI;

    SchemaResourceResolver(String currentDir, String scriptFileName, Object input, SchemaErrorHandler errorHandler,
                           boolean noNet)
    {
      this.currentDir = currentDir;
      this.scriptFileName = scriptFileName;
      this.errorHandler = errorHandler;
      this.noNet = noNet;
      if (input == null) { return; }
      if (input instanceof String) {
        lsInput.setStringData((String)input);
      } else if (input instanceof Reader) {
        lsInput.setCharacterStream((Reader)input);
      } else if (input instanceof InputStream) {
        lsInput.setByteStream((InputStream)input);
      }
    }

    @Override
    public LSInput
    resolveResource(String type,
                    String namespaceURI,
                    String publicId,
                    String systemId,
                    String baseURI)
    {
      if (noNet && systemId != null && (systemId.startsWith("http://") || systemId.startsWith("ftp://"))) {
        if (systemId.startsWith(XMLConstants.W3C_XML_SCHEMA_NS_URI)) {
          return null; // use default resolver
        }
        try {
          this.errorHandler.warning(new SAXParseException(String.format("Attempt to load network entity '%s'", systemId), null));
        } catch (SAXException ex) {
        }
      } else {
        String adjusted = adjustSystemIdIfNecessary(currentDir, scriptFileName, baseURI, systemId);
        lsInput.setPublicId(publicId);
        lsInput.setSystemId(adjusted != null ? adjusted : systemId);
        lsInput.setBaseURI(baseURI);
      }
      return lsInput;
    }
  }

  private class SchemaLSInput implements LSInput
  {
    protected String fPublicId;
    protected String fSystemId;
    protected String fBaseSystemId;
    protected InputStream fByteStream;
    protected Reader fCharStream;
    protected String fData;
    protected String fEncoding;
    protected boolean fCertifiedText = false;

    @Override
    public String
    getBaseURI()
    {
      return fBaseSystemId;
    }

    @Override
    public InputStream
    getByteStream()
    {
      return fByteStream;
    }

    @Override
    public boolean
    getCertifiedText()
    {
      return fCertifiedText;
    }

    @Override
    public Reader
    getCharacterStream()
    {
      return fCharStream;
    }

    @Override
    public String
    getEncoding()
    {
      return fEncoding;
    }

    @Override
    public String
    getPublicId()
    {
      return fPublicId;
    }

    @Override
    public String
    getStringData()
    {
      return fData;
    }

    @Override
    public String
    getSystemId()
    {
      return fSystemId;
    }

    @Override
    public void
    setBaseURI(String baseURI)
    {
      fBaseSystemId = baseURI;
    }

    @Override
    public void
    setByteStream(InputStream byteStream)
    {
      fByteStream = byteStream;
    }

    @Override
    public void
    setCertifiedText(boolean certified)
    {
      fCertifiedText = certified;
    }

    @Override
    public void
    setCharacterStream(Reader charStream)
    {
      fCharStream = charStream;
    }

    @Override
    public void
    setEncoding(String encoding)
    {
      fEncoding = encoding;
    }

    @Override
    public void
    setPublicId(String pubId)
    {
      fPublicId = pubId;
    }

    @Override
    public void
    setStringData(String stringData)
    {
      fData = stringData;
    }

    @Override
    public void
    setSystemId(String sysId)
    {
      fSystemId = sysId;
    }

  }
}
