package eu.blogforever.mfextractor;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import org.deri.any23.extractor.DocumentContext;
import org.deri.any23.extractor.ExtractionContext;
import org.deri.any23.extractor.ExtractionException;
import org.deri.any23.extractor.ExtractionResultImpl;
import org.deri.any23.extractor.Extractor;
import org.deri.any23.extractor.ExtractorFactory;
import org.deri.any23.extractor.ExtractorGroup;
import org.deri.any23.extractor.ExtractorRegistry;
import org.deri.any23.extractor.Extractor.TagSoupDOMExtractor;
import org.deri.any23.extractor.TagSoupExtractionResult.PropertyPath;
import org.deri.any23.extractor.TagSoupExtractionResult.ResourceRoot;
import org.deri.any23.mime.MIMEType;
import org.deri.any23.util.RDFHelper;
import org.deri.any23.validator.ValidatorException;
import org.deri.any23.vocab.SINDICE;
import org.deri.any23.writer.NTriplesWriter;
import org.deri.any23.writer.RDFXMLWriter;
import org.deri.any23.writer.TripleHandler;
import org.deri.any23.writer.TripleHandlerException;
import org.deri.any23.writer.TurtleWriter;
import org.openrdf.model.BNode;
import org.openrdf.model.impl.ValueFactoryImpl;

/**
 * Extracts microformats from a HTML document. Typically it's a facade of the extraction process 
 * over a {@link eu.blog4ever.altec.ExtractionInput}. 
 * It's heavily based on Any23 library (http://developers.any23.org/getting-started.html), 
 * and supports the following microformats: 
 * Adr, Geo, hCalendar, hCard, hListing, hResume, hReview, License, XFN and Species.
 * 
 * The result of extraction proccess provided in multiple formats, such as RDF/XML,
 * Turtle (subset of N3), and NTriples.
 * 
 * Extraction proccess is thread-safe and the same instance of MicroformatExtractor can
 * use concurrently from multiple threads. For performance reasons it is recommended that 
 * each thread has his own instance of MicroformatExtractor. 
 * 
 * 
 */
public class MicroformatExtractor {	
	
	
	private static final String DOMAIN = "domain";
    private static final String NESTING = "nesting";
    private static final String NESTING_ORIGINAL = "nesting_original";
    private static final String NESTING_STRUCTURED_PROPERTY = "nesting_structured";
    
	static private MIMEType mime = MIMEType.parse("text/html;");
	
	
	ExtractionInput input;
	
	TripleHandler outputHandler = null;
	//ByteArrayOutputStream out = null;
	
	// A group of extractors that match with the specific mimetype.
	// Filter extractors by text/html mime type and keep only a relevant subset.
	ExtractorGroup matchingExtractors = 
		ExtractorRegistry.getInstance().getExtractorGroup().filterByMIMEType(mime);
	
	List<ResourceRoot> resourceRoots = null;
    List<PropertyPath> propertyPaths = null;
    
    /**
     * Extract all microformats from a specific document as RDF/XML. 
     * 
     * @param input A document as a {@link eu.blog4ever.altec.ExtractionInput}
     * @param output Output of Extraction Proccess as {@link java.io.OutputStream}
     * 
     * @throws ExtractionException
     * @throws IOException
     * @throws ValidatorException
     * @throws TripleHandlerException
     */
	public void extractToRDFXML(ExtractionInput input, OutputStream output) 
			throws ExtractionException, IOException, ValidatorException, TripleHandlerException{
		synchronized(this){
			this.input = input;
			outputHandler = new RDFXMLWriter(output);
			resourceRoots = new ArrayList<ResourceRoot>();
			propertyPaths = new ArrayList<PropertyPath>();
			extract();
			outputHandler.close();
		}
	}
	
	/**
     * Extract all microformats from a specific document as Turtle
     * 
     * @param input A document as a {@link eu.blog4ever.altec.ExtractionInput}
     * @param output Output of Extraction Proccess as {@link java.io.OutputStream}
     * 
     * @throws ExtractionException
     * @throws IOException
     * @throws ValidatorException
     * @throws TripleHandlerException
     */
	public void extractToTurtle(ExtractionInput input, OutputStream output) 
			throws ExtractionException, IOException, ValidatorException, TripleHandlerException{
		synchronized(this){
			this.input = input;
			outputHandler = new TurtleWriter(output);
			resourceRoots = new ArrayList<ResourceRoot>();
			propertyPaths = new ArrayList<PropertyPath>();
			extract();
			outputHandler.close();
		}
	}
	
	/**
     * Extract all microformats from a specific document as a set of Triples.
     * 
     * @param input A document as a {@link eu.blog4ever.altec.ExtractionInput}
     * @param output Output of Extraction Proccess as {@link java.io.OutputStream}
     * 
     * @throws ExtractionException
     * @throws IOException
     * @throws ValidatorException
     * @throws TripleHandlerException
     */
	public void extractToNTriples(ExtractionInput input, OutputStream output) 
			throws ExtractionException, IOException, ValidatorException, TripleHandlerException{
		synchronized(this){
			this.input = input;
			outputHandler = new NTriplesWriter(output);
			resourceRoots = new ArrayList<ResourceRoot>();
			propertyPaths = new ArrayList<PropertyPath>();
			extract();
			outputHandler.close();
		}
	}
	
	private void extract() 
			throws ExtractionException, IOException, ValidatorException{
		
		//filterExtractorGroup(matchingExtractors);
		
		// Run extraction process for each extractor in matchingExtractors.
		final DocumentContext documentContext = new DocumentContext(input.getLanguage());
		
		for (ExtractorFactory<?> factory : matchingExtractors) {
            runExtractor(documentContext, factory.createExtractor());
        }
	}
	
	
	private void runExtractor(
            DocumentContext documentContext,
            Extractor<?> extractor
    ) throws ExtractionException, IOException, ValidatorException {
        
        
        ExtractionResultImpl result = new ExtractionResultImpl(
        		documentContext, input.getDocumentURI(), extractor, outputHandler);
        try {
        	
            if (extractor instanceof TagSoupDOMExtractor) {
                final TagSoupDOMExtractor tagSoupDOMExtractor = (TagSoupDOMExtractor) extractor;
                tagSoupDOMExtractor.run(input.getDom(), input.getDocumentURI(), result);
            } else {
                throw new RuntimeException("Extractor type not supported: " + extractor.getClass());
            }
            
            resourceRoots.addAll( result.getResourceRoots() );
            propertyPaths.addAll( result.getPropertyPaths() );
            
        } catch (ExtractionException ex) {
            throw ex;
        } finally {
            result.close();
        }
        
        consolidateResources(resourceRoots, propertyPaths, outputHandler);
        
    }
	
	/**
	 * Code of this method is part of any23 library used unchanged.
	 * 
     * Consolidates the graphs extracted from the same document.
     * In particular it adds:
     * <ul>
     *   <li>for every microformat root node a triple indicating the original Web page domain;</li>
     *   <li>triples indicating the nesting relationship among a microformat root and property paths of
     *       other nested microformats.
     *   </li>
     * </ul>
     * @param resourceRoots list of RDF nodes representing roots of
     *        extracted microformat graphs and the corresponding HTML paths.
     * @param propertyPaths list of RDF nodes representing property subjects, property URIs and the HTML paths
     *        from which such properties have been extracted. 
     * @param output a triple handler event collector.
     * @throws ExtractionException
     * 
     */
     
	private void consolidateResources(
            List<ResourceRoot> resourceRoots,
            List<PropertyPath> propertyPaths,
            TripleHandler output
    ) throws ExtractionException {
        
		final ExtractionContext context = new ExtractionContext(
                "consolidation-extractor",
                input.getDocumentURI(),
                UUID.randomUUID().toString()
        );

        try {
            output.openContext(context);
        } catch (TripleHandlerException e) {
            throw new ExtractionException(String.format("Error starting document with URI %s", input.getDocumentURI()), e );
        }
        try {
            // Add source Web domains to every resource root.
            final String domain;
            try {
                domain = new java.net.URI( input.getDocumentURI().stringValue() ).getHost();
            } catch (URISyntaxException urise) {
                throw new IllegalArgumentException(
                        "An error occurred while extracting the host from the document URI.",
                        urise
                );
            }
            if (domain != null) {
                for (ResourceRoot resourceRoot : resourceRoots) {
                    output.receiveTriple(
                    		
                            resourceRoot.getRoot(),
                            SINDICE.getProperty(DOMAIN),
                            ValueFactoryImpl.getInstance().createLiteral(domain),
                            null,
                            context
                    );
                }
            }

            // Detect the nesting relationship among different microformats and explicit them adding connection triples.
            ResourceRoot currentResourceRoot;
            PropertyPath currentPropertyPath;
            for(int r = 0; r < resourceRoots.size(); r++) {
                currentResourceRoot = resourceRoots.get(r);
                for(int p = 0; p < propertyPaths.size(); p++) {
                    currentPropertyPath = propertyPaths.get(p);
                    // Avoid wrong nesting relationships.
                    if(currentPropertyPath.getExtractor().equals(currentResourceRoot.getExtractor())) {
                        continue;
                    }
                    if( subPath( currentResourceRoot.getPath(), currentPropertyPath.getPath() ) ) {
                        createNestingRelationship(currentPropertyPath, currentResourceRoot, output, context);
                    }
                }
            }
        } catch (TripleHandlerException e) {
            throw new ExtractionException("Error while writing triple triple.", e);
        } finally {
            try {
                output.closeContext(context);
            } catch (TripleHandlerException e) {
                throw new ExtractionException("Error while closing context.", e);
            }
        }
    }
	
	private boolean subPath(String[] list, String[] candidateSub) {
        if(candidateSub.length > list.length) {
            return false;
        }
        for(int i = 0; i < candidateSub.length; i++) {
            if( ! candidateSub[i].equals(list[i])) {
                return false;
            }
        }
        return true;
    }
    
	/**
	 * 
	 * Code of this method is part of any23 library used unchanged.
	 * 
     * Creates a nesting relationship triple.
     * 
     * @param from the property containing the nested microformat.
     * @param to the root to the nested microformat.
     * @param th the triple handler.
     * @param ec the extraction context used to add such information.
     * @throws org.deri.any23.writer.TripleHandlerException
     * 
     */
    private void createNestingRelationship(PropertyPath from, ResourceRoot to, TripleHandler th, ExtractionContext ec)
    throws TripleHandlerException {
        final BNode fromObject = from.getObject();
        final String bNodeHash = from.getProperty().stringValue() + ( fromObject == null ? "" : fromObject.getID() );
        BNode bnode = RDFHelper.getBNode(bNodeHash);
        th.receiveTriple(bnode, SINDICE.getProperty(NESTING_ORIGINAL), from.getProperty(), null, ec );
        th.receiveTriple(
                bnode,
                SINDICE.getProperty(NESTING_STRUCTURED_PROPERTY),
                from.getObject() == null ? to.getRoot() : from.getObject(),
                null,
                ec
        );
        th.receiveTriple(
                from.getSubject(),
                SINDICE.getProperty(NESTING),
                bnode,
                null,
                ec
        );
    }	

}
