package an.xacml.adapter.file;

import static an.xacml.adapter.file.XACMLParser.createPolicyDataAdapterFromXMLElement;
import static an.xacml.adapter.file.XACMLParser.getPolicyDefaultSchema;
import static an.xml.XMLParserWrapper.getNodeXMLText;
import static an.xml.XMLParserWrapper.parse;
import static an.xml.XMLParserWrapper.verifySchemaFile;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.ws.wsaddressing.W3CEndpointReference;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import an.config.ConfigElement;
import an.config.ConfigurationException;
import an.xacml.IPDPInjectable;
import an.xacml.PolicySyntaxException;
import an.xacml.XACMLElement;
import an.xacml.adapter.DataAdapter;
import an.xacml.adapter.DataAdapterException;
import an.xacml.engine.BuiltInFunctionNotFoundException;
import an.xacml.engine.CacheManager;
import an.xacml.engine.CacheSizeExceedCapacityException;
import an.xacml.engine.DataStore;
import an.xacml.engine.PDP;
import an.xacml.policy.AbstractPolicy;
import an.xml.XMLGeneralException;
import eu.dnetlib.enabling.aas.retrievers.resultset.IResultSetProvider;
import eu.dnetlib.enabling.aas.service.AASConstants;
import eu.dnetlib.enabling.aas.utils.dump.IDumpable;
import eu.dnetlib.enabling.aas.utils.invalidate.IInvalidatable;
import eu.dnetlib.enabling.aas.utils.invalidate.InvalidationException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.enabling.resultset.ResultSetConstants;
import eu.dnetlib.enabling.resultset.rmi.ResultSetException;
import eu.dnetlib.enabling.resultset.rmi.ResultSetService;
import eu.dnetlib.enabling.tools.ServiceLocator;

/**
 * {@link DataStore} implementation using ISLookup to retrieve policies.
 * 
 * @author mhorst
 *
 */
public class ISLookupDataStore implements DataStore, IPDPInjectable, ErrorHandler, 
	IDumpable, IInvalidatable {

	
	private final Logger logger = Logger.getLogger(this.getClass());
	
	/**
	 * PDP required for policy building.
	 */
	private PDP pdp;
	
	/**
	 * ISLookup service locator.
	 */
	private ServiceLocator<ISLookUpService> lookupLocator;
	
	/**
	 * ResultSet provider module. Injected implementation may use service caching etc.
	 * As a bonus makes testing much easier.
	 */
	private IResultSetProvider resultSetProvider;
	
	/**
	 * Flag indicating proper namespace should be set in retrieved policy.
	 */
	private boolean resetPolicyNamespace = true;
	
	private Set<URI> policyIDs = new HashSet<URI>();

    public static final String ATTRIBUTE_POLICY_ID = "PolicyId";
    public static final String ATTRIBUTE_POLICYSET_ID = "PolicySetId";
	

    public static final String JAXP_SCHEMA_LANGUAGE =
        "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    
    public static final String W3C_XML_SCHEMA =
        "http://www.w3.org/2001/XMLSchema";

    public static final String JAXP_SCHEMA_SOURCE =
        "http://java.sun.com/xml/jaxp/properties/schemaSource";
    
    public static final String XACML20_NS_KEY = "xmlns";
    
    public static final String XACML20_NS_VALUE = "urn:oasis:names:tc:xacml:2.0:policy:schema:os";
    
    /**
     * Internal document builder for creating policies elements.
     */
    protected DocumentBuilder documentBuilder;
    
    /**
     * DOM Element transformer.
     */
    protected Transformer transformer;
    
    
	/**
	 * Default constructor.
	 * @throws ParserConfigurationException 
	 * @throws TransformerConfigurationException 
	 */
	public ISLookupDataStore() throws ParserConfigurationException, 
		TransformerConfigurationException {
//		no schema file is provided, IS validates profiles
		documentBuilder = prepareDocumentBuilder(null);
//		transformer is required to convert DOM Element into String
		TransformerFactory tf = TransformerFactory.newInstance();
		transformer = tf.newTransformer();
	}
	
	/**
	 * Introduced to sustain compatibility with XACML configuration.
	 * @param config
	 * @throws ParserConfigurationException 
	 * @throws TransformerConfigurationException 
	 */
	public ISLookupDataStore(ConfigElement config) throws ParserConfigurationException, 
		TransformerConfigurationException {
		this();
	}
	
	/**
	 * Prepares main document builder.
	 * @param schemaFile optional schema file
	 * @return prepared {@link DocumentBuilder}
	 * @throws ParserConfigurationException 
	 */
	protected DocumentBuilder prepareDocumentBuilder(File schemaFile) 
		throws ParserConfigurationException {
		 // create the factory
        DocumentBuilderFactory factory =
            DocumentBuilderFactory.newInstance();
        factory.setIgnoringComments(true);
        DocumentBuilder db = null;
        // as of 1.2, we always are namespace aware
        factory.setNamespaceAware(true);
        // set the factory to work the way the system requires
        if (schemaFile == null) {
            // we're not doing any validation
            factory.setValidating(false);
            db = factory.newDocumentBuilder();
        } else {
            // we're using a validating parser
            factory.setValidating(true);
            factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
            factory.setAttribute(JAXP_SCHEMA_SOURCE, schemaFile);
            db = factory.newDocumentBuilder();
            db.setErrorHandler(this);
        }
        return db;
	}
	
	/* (non-Javadoc)
	 * @see an.xacml.engine.DataStore#load()
	 */
	@Override
	public DataAdapter[] load() throws DataAdapterException {
        try {
            policyIDs.clear();

            logger.info("loading policies using ISLookup...");

//          no need to verify against the schema, it will be performed by IS
//          Retrieve the schema file from given path or classpath, and verify if it exists.
//          String defaultSchema = verifySchemaFile(getPolicyDefaultSchema());

            System.gc();
            long memBegin = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
            long begin = System.currentTimeMillis();
            
//          loading policies from ISLookup
            List<DataAdapter> adapters = loadPolicies(getPolicyProfilesFromIS());
            
            // Print some hints
            long end = System.currentTimeMillis();
            System.gc();
            long memEnd = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
            logger.info(adapters.size() + " policies loaded. " +
                    "Time elapsed " + (end - begin) / 1000 + " second. " +
                    "Memory used " + (memEnd - memBegin) / 1024 / 1024 + " MB.");
            return adapters.toArray(new DataAdapter[adapters.size()]);
        } catch (DataAdapterException daEx) {
            throw daEx;
        } catch (Exception ex) {
            throw new DataAdapterException("Exception occured when loading policies", ex);
        }
	}

	/**
	 * Loads policies from ISLookup service. Never returns null.
	 * @param policyProfiles array of policy profiles
	 * @return list of policies
	 * @throws DataAdapterException
	 */
	protected List<DataAdapter> loadPolicies(String[] policyProfiles) throws DataAdapterException {
		if (policyProfiles!=null && policyProfiles.length>0) {
			String defaultSchema = null;
			try {
				defaultSchema = verifySchemaFile(getPolicyDefaultSchema());
			} catch (XMLGeneralException e1) {
				throw new DataAdapterException(
						"unable to verify schema file", e1);
			} catch (ConfigurationException e1) {
				throw new DataAdapterException(
						"unable to verify schema file", e1);
			}
			
			List<DataAdapter> policies = new ArrayList<DataAdapter>(policyProfiles.length);
			for (String policyProfile : policyProfiles) {
				try {
					Document doc = documentBuilder.parse(
							new InputSource(new StringReader(policyProfile)));
//		            handle the policy, if it's a known type
		            Element rootCandidate = getPolicyRootElement(doc);
		            if (rootCandidate==null) {
		            	throw new DataAdapterException(
		            			"Couldn't find policy root element in " +
		            			"resourceProfile: " + policyProfile);
		            }
		            
		            if (resetPolicyNamespace) {
//		            	setting XACML2.0 xmlns schema, which had to be removed when registering profile into IS
		            	rootCandidate.setAttribute(XACML20_NS_KEY, XACML20_NS_VALUE);
		            }
		            
//		            it is really sad to do such things, XACML lib is crap...
	            	StringWriter sw = new StringWriter();
	            	transformer.transform(new DOMSource(rootCandidate), new StreamResult(sw));
		            Element root = parse(new ByteArrayInputStream(
		            		sw.toString().getBytes()), 
		            		defaultSchema);
		            
//		            check if there are duplicated policy IDs in different files
                    URI policyId = getPolicyOrPolicySetId(root);
                    if (policyIDs.contains(policyId)) {
                        throw new DataAdapterException("The loaded policy " +
                                        "with ID<" + policyId + "> already exists.");
                    }

                    policyIDs.add(policyId);
                    DataAdapter da = createPolicyDataAdapterFromXMLElement(root);
//                  We may use configuration of PDP in engine element.
                    ((AbstractPolicy)da.getEngineElement()).setOwnerPDP(pdp);
                    policies.add(da);

                    // Check if loaded policy is correct
                    if (logger.isDebugEnabled()) {
                        try {
                            // Create a new dataAdapter, do not use the loaded one, since it holds the original
                            // XML element. We should use the one data adapter generated.
                            XACMLElement engineElem = da.getEngineElement();
                            Constructor<?> cons = da.getClass().getConstructor(XACMLElement.class);
                            DataAdapter daGenFromEngineElem = (DataAdapter)cons.newInstance(engineElem);
                            logger.debug("policy dump: " +
                                    getNodeXMLText((Element)daGenFromEngineElem.getDataStoreObject()));
                        }
                        catch (Exception debugEx) {
                            logger.debug("Dump policy failed due to: ", debugEx);
                        }
                    }
		            
				} catch (SAXException e) {
					throw new DataAdapterException(
	            			"Exception occured when processing " +
	            			"resourceProfile: " + policyProfile);
				} catch (IOException e) {
					throw new DataAdapterException(
	            			"Exception occured when processing " +
	            			"resourceProfile: " + policyProfile);
				} catch (URISyntaxException e) {
					throw new DataAdapterException(
							"unable to extract policy(set) id uri " +
							"from policy(set) content", e);
				} catch (PolicySyntaxException e) {
					throw new DataAdapterException(
							"unable to extract policy(set) id uri " +
							"from policy(set) content", e);
				} catch (Exception e) {
					throw new DataAdapterException(
							"unable to load policy: " +
							policyProfile, e);
				}
			}
			return policies;
		} else {
			logger.debug("no policies loaded from IS");
			return Collections.<DataAdapter>emptyList();
		}
	}
	
	/**
     * Get policy id if parsed Element is a Policy, or get policySet id if parsed Element is a PolicySet.
     * There should not have multiple policies or policySets in a single file.
     * @param element
     * @return
     * @throws URISyntaxException
     * @throws PolicySyntaxException
     */
    private URI getPolicyOrPolicySetId(Element element) throws URISyntaxException, PolicySyntaxException {
        String id = element.getAttribute(ATTRIBUTE_POLICY_ID);
        if (id == null || "".equals(id)) {
            id = element.getAttribute(ATTRIBUTE_POLICYSET_ID);
        }
        if (id == null || "".equals(id)) {
            // This should not be happened since we have checked schema while parsing.
            throw new PolicySyntaxException("The element '" + element.getLocalName() +
                    "' doesn't include PolicyId or PolicySetId attribute.");
        }
        return new URI(id);
    }

	
	/**
     * Returns Policy or PolicySet element from resourceProfile document.
     * @param doc
     * @return Policy or PolicySet element.
     */
    Element getPolicyRootElement(Document doc) {
    	if (doc==null)
    	return null;
    	Element documentElement = doc.getDocumentElement();
    	NodeList nodeList = documentElement.getElementsByTagName("CONFIGURATION");
    	if (nodeList.getLength()!=1) {
    		logger.error("Invalid list of nodes for CONFIGURATION element. " +
    				"Expected 1, found: " + nodeList.getLength());
    		return null;
    	}
    	Element configurationElement = (Element) nodeList.item(0);
    	if (configurationElement==null) {
    		logger.error("Couldn't find CONFIGURATION element!");
    		return null;
    	}
    	NodeList childNodes = configurationElement.getChildNodes();
    	if (childNodes.getLength()>1) {
//    		there can be some whitespaces which should be ommited 
    		int currentNodeIndex = 0;
    		while (currentNodeIndex < childNodes.getLength()) {
    			Node currentNode = childNodes.item(currentNodeIndex);
    			if (currentNode.getLocalName()!=null)
    				if (currentNode.getLocalName().equals("Policy") ||
    						currentNode.getLocalName().equals("PolicySet"))
    					return (Element) currentNode;
    			currentNodeIndex++;
    		}
    		return null;
    		
    	} else if (childNodes.getLength()==1)
    		return (Element) configurationElement.getFirstChild();
    	else
    		return null;
    }
	
	/**
	 * Retrieves PolicyProfiles from IS.
	 * @return array of policyProfiles
	 */
	@SuppressWarnings("deprecation")
	private String[] getPolicyProfilesFromIS() {
		
		try {
			W3CEndpointReference rsIdXml = null;
			rsIdXml = lookupLocator.getService().listResourceProfiles(
					AASConstants.RESOURCE_KIND_SECURITY_POLICY,
					null, AASConstants.RESOURCE_TYPE_SECURITY_POLICY);

			List<String> array1Res = null;
			
			String rsId = resultSetProvider.extractId(rsIdXml);
			if (rsId==null) {
				logger.error("found no rsId in W3CEndpointReference!");
				return null;
			}
			ResultSetService resultSetService = resultSetProvider.discover(rsIdXml);
			int resultsCount = resultSetService.getNumberOfElements(rsId);
			if (resultsCount==0) {
				return new String[0];
			}
			array1Res = resultSetService.getResult(rsId, ResultSetConstants.RESULT_SET_FIRST_ELEMENT, 
					resultsCount, ResultSetConstants.RESULT_SET_REQUEST_MODE_WAITING);
			if (array1Res==null) {
				return null;
			} else {
				return array1Res.toArray(new String[0]);
			}
		} catch (ResultSetException e) {
			logger.error("Exception occured when retrieving results from " +
					"result service!", e);
			return null;
		} catch (ISLookUpException e) {
			logger.error("Exception occured when loading profiles from " +
					"ISLookup service!", e);
			return null;
		}
		
	}
	
	/* (non-Javadoc)
	 * @see an.xacml.engine.DataStore#save()
	 */
	@Override
	public void save() throws DataAdapterException {
		throw new DataAdapterException("saving policies is unsupported in " +
                this.getClass().getCanonicalName());
	}

	/* (non-Javadoc)
	 * @see an.xacml.engine.DataStore#update(an.xacml.adapter.DataAdapter[], an.xacml.adapter.DataAdapter[])
	 */
	@Override
	public void update(DataAdapter[] arg0, DataAdapter[] arg1)
			throws DataAdapterException {
		throw new DataAdapterException("updating policies is unsupported in " +
                this.getClass().getCanonicalName());
	}
	
	/* (non-Javadoc)
	 * @see an.xacml.engine.DataStore#shutdown()
	 */
	@Override
	public void shutdown() {
		policyIDs.clear();
	}

	/**
     * Standard handler routine for the XML parsing.
     *
     * @param exception information on what caused the problem
     */
    public void warning(SAXParseException exception) throws SAXException {
        logger.warn("Warning on line " + exception.getLineNumber() +
                           ": " + exception.getMessage());
    }

    /**
     * Standard handler routine for the XML parsing.
     *
     * @param exception information on what caused the problem
     *
     * @throws SAXException always to halt parsing on errors
     */
    public void error(SAXParseException exception) throws SAXException {
       logger.error("Error on line " + exception.getLineNumber() +
                           ": " + exception.getMessage() + " ... " +
                           "Policy will not be available");

        throw new SAXException("error parsing policy");
    }

    /**
     * Standard handler routine for the XML parsing.
     *
     * @param exception information on what caused the problem
     *
     * @throws SAXException always to halt parsing on errors
     */
    public void fatalError(SAXParseException exception) throws SAXException {
        logger.error("Fatal error on line " + exception.getLineNumber() +
                           ": " + exception.getMessage() + " ... " +
                           "Policy will not be available");

        throw new SAXException("fatal error parsing policy");
    }

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.utils.dump.IDumpable#dump(java.io.OutputStream, java.lang.Object[])
	 */
	@Override
	public void dump(OutputStream out, Object... params) throws IOException {
		try {
			AbstractPolicy[] policies = CacheManager.getInstance(pdp).getAllPolicies();
			for (AbstractPolicy policy : policies) {
				out.write(policy.getId().toASCIIString().getBytes());
				out.write('\n');
			}
		} catch (XMLGeneralException e) {
			logger.error("exception occured when listing policies!", e);
			out.write(e.getMessage().getBytes());
		}	
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.enabling.aas.utils.invalidate.IInvalidatable#invalidate()
	 */
	@Override
	public void invalidate() throws InvalidationException {
		try {
			this.pdp.reloadPolicies();
		} catch (DataAdapterException e) {
			throw new InvalidationException("reloading policies failed!" ,e);
		} catch (CacheSizeExceedCapacityException e) {
			throw new InvalidationException("reloading policies failed!" ,e);
		} catch (PolicySyntaxException e) {
			throw new InvalidationException("reloading policies failed!" ,e);
		} catch (BuiltInFunctionNotFoundException e) {
			throw new InvalidationException("reloading policies failed!" ,e);
		}
	}
	
	/* (non-Javadoc)
	 * @see an.xacml.IPDPInjectable#setPdp(an.xacml.engine.PDP)
	 */
	@Override
	public void setPdp(PDP pdp) {
		this.pdp = pdp;
	}

	/**
	 * Sets ISLookup service locator.
	 * @param lookupLocator
	 */
	public void setLookupLocator(ServiceLocator<ISLookUpService> lookupLocator) {
		this.lookupLocator = lookupLocator;
	}

	/**
	 * Sets ResultSet provider.
	 * @param resultSetProvider
	 */
	public void setResultSetProvider(IResultSetProvider resultSetProvider) {
		this.resultSetProvider = resultSetProvider;
	}

}
