/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.olingo.client.core.serialization;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;

import org.apache.olingo.client.api.data.ResWrap;
import org.apache.olingo.client.api.data.ServiceDocument;
import org.apache.olingo.client.api.edm.xml.Edmx;
import org.apache.olingo.client.api.edm.xml.XMLMetadata;
import org.apache.olingo.client.api.serialization.ClientODataDeserializer;
import org.apache.olingo.client.api.serialization.ODataDeserializer;
import org.apache.olingo.client.api.serialization.ODataDeserializerException;
import org.apache.olingo.client.core.data.JSONServiceDocumentDeserializer;
import org.apache.olingo.client.core.data.XMLServiceDocumentDeserializer;
import org.apache.olingo.client.core.edm.ClientCsdlXMLMetadata;
import org.apache.olingo.client.core.edm.xml.ClientCsdlEdmx;
import org.apache.olingo.commons.api.data.Delta;
import org.apache.olingo.commons.api.data.Entity;
import org.apache.olingo.commons.api.data.EntityCollection;
import org.apache.olingo.commons.api.data.Property;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.provider.CsdlSchema;
import org.apache.olingo.commons.api.ex.ODataError;
import org.apache.olingo.commons.api.format.ContentType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.fasterxml.aalto.stax.InputFactoryImpl;
import com.fasterxml.aalto.stax.OutputFactoryImpl;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

public class ClientODataDeserializerImpl implements ClientODataDeserializer {

  private final ODataDeserializer deserializer;
  private final ContentType contentType;
  private static final String SCHEMA = "Schema";
  private static final String XMLNS = "xmlns";

  public ClientODataDeserializerImpl(final boolean serverMode, final ContentType contentType) {
    this.contentType = contentType;
    if (contentType.isCompatible(ContentType.APPLICATION_ATOM_SVC)
        || contentType.isCompatible(ContentType.APPLICATION_ATOM_XML)
        || contentType.isCompatible(ContentType.APPLICATION_XML)) {
      deserializer = new AtomDeserializer();
    } else {
      deserializer = new JsonDeserializer(serverMode);
    }
  }

  @Override
  public ResWrap<EntityCollection> toEntitySet(final InputStream input) throws ODataDeserializerException {
    return deserializer.toEntitySet(input);
  }

  @Override
  public ResWrap<Entity> toEntity(final InputStream input) throws ODataDeserializerException {
    return deserializer.toEntity(input);
  }

  @Override
  public ResWrap<Property> toProperty(final InputStream input) throws ODataDeserializerException {
    return deserializer.toProperty(input);
  }

  @Override
  public ODataError toError(final InputStream input) throws ODataDeserializerException {
    return deserializer.toError(input);
  }

  protected XmlMapper getXmlMapper() {
    final XmlMapper xmlMapper = new XmlMapper(
        new XmlFactory(new InputFactoryImpl(), new OutputFactoryImpl()), new JacksonXmlModule());

    xmlMapper.setInjectableValues(new InjectableValues.Std().addValue(Boolean.class, Boolean.FALSE));

    xmlMapper.addHandler(new DeserializationProblemHandler() {
      @Override
      public boolean handleUnknownProperty(final DeserializationContext ctxt, final JsonParser jp,
          final com.fasterxml.jackson.databind.JsonDeserializer<?> deserializer,
          final Object beanOrClass, final String propertyName)
          throws IOException, JsonProcessingException {

        // skip any unknown property
        ctxt.getParser().skipChildren();
        return true;
      }
    });
    return xmlMapper;
  }

  @Override
  public XMLMetadata toMetadata(final InputStream input) {
    try {
    	
    	ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    	org.apache.commons.io.IOUtils.copy(input, byteArrayOutputStream);
    	// copy the content of input stream to reuse it
    	byte[] inputContent = byteArrayOutputStream.toByteArray();
    	
    	InputStream inputStream1 = new ByteArrayInputStream(inputContent);
    	Edmx edmx = getXmlMapper().readValue(inputStream1, ClientCsdlEdmx.class);
    	
    	InputStream inputStream2 = new ByteArrayInputStream(inputContent);
    	List<List<String>> schemaNameSpaces = getAllSchemaNameSpace(inputStream2);
 
      return new ClientCsdlXMLMetadata(edmx,schemaNameSpaces);
    } catch (Exception e) {
      throw new IllegalArgumentException("Could not parse as Edmx document", e);
    }
  }

	private List<List<String>> getAllSchemaNameSpace(InputStream inputStream)
			throws ParserConfigurationException, SAXException, IOException{
		List<List<String>> schemaNameSpaces = new ArrayList <>();
		
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		dbFactory.setFeature(
	                "http://xml.org/sax/features/namespaces", true);
		dbFactory.setFeature(
	                "http://apache.org/xml/features/validation/schema",
	                false);
		dbFactory.setFeature(
	                "http://apache.org/xml/features/validation/schema-full-checking",
	                true);
		dbFactory.setFeature(
	                "http://xml.org/sax/features/external-general-entities",
	                false);
		dbFactory.setFeature(
	                "http://xml.org/sax/features/external-parameter-entities",
	                false);
		dbFactory.setFeature(
	                "http://apache.org/xml/features/disallow-doctype-decl",
	                true);
		dbFactory.setFeature(
	                "http://javax.xml.XMLConstants/feature/secure-processing",
	                true);
		
		DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		Document doc = dBuilder.parse(inputStream);
		doc.getDocumentElement().normalize();
		NodeList nList = doc.getElementsByTagName(SCHEMA);
		
		for (int temp = 0; temp < nList.getLength(); temp++) {
			Node nNode = nList.item(temp);
			List<String> nameSpaces = new ArrayList <>();
			if (nNode.getNodeType() == Node.ELEMENT_NODE) {
				Element eElement = (Element) nNode;
				NamedNodeMap attributes = eElement.getAttributes();
				int len = attributes.getLength();
				for(int i =0;i<len;i++){
					// check for all atributes begining with name xmlns or xmlns:
					String attrName = attributes.item(i).getNodeName();
					if( XMLNS.equals(attrName) || attrName.startsWith(XMLNS+":")){
						nameSpaces.add(attributes.item(i).getNodeValue());
					}
				}
			}
			schemaNameSpaces.add(nameSpaces);
		}
	return schemaNameSpaces;
	}

  @Override
  public ResWrap<ServiceDocument> toServiceDocument(final InputStream input) throws ODataDeserializerException {
    return contentType.isCompatible(ContentType.APPLICATION_XML) ?
        new XMLServiceDocumentDeserializer(false).toServiceDocument(input) :
        new JSONServiceDocumentDeserializer(false).toServiceDocument(input);
  }

  @Override
  public ResWrap<Delta> toDelta(final InputStream input) throws ODataDeserializerException {
    try {
      return contentType.isCompatible(ContentType.APPLICATION_ATOM_SVC)
          || contentType.isCompatible(ContentType.APPLICATION_ATOM_XML) ?
          new AtomDeserializer().delta(input) :
          new JsonDeltaDeserializer(false).toDelta(input);
    } catch (final XMLStreamException | EdmPrimitiveTypeException e) {
      throw new ODataDeserializerException(e);
    }
  }

  @Override
  public List<CsdlSchema> fetchTermDefinitionSchema(List<InputStream> input) {
    List<CsdlSchema> schemas = new ArrayList<>();
    try {
      for (InputStream stream : input) {
        ClientCsdlEdmx edmx = getXmlMapper().readValue(stream, ClientCsdlEdmx.class);
        schemas.addAll(edmx.getDataServices().getSchemas());
      }
      return schemas;
    } catch (Exception e) {
      throw new IllegalArgumentException("Could not parse Term definition", e);
    }
  }
}
