/**
 * Copyright 2005-2016 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.ken.util;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.rice.core.api.config.property.ConfigContext;
import org.kuali.rice.core.api.criteria.Predicate;
import org.kuali.rice.core.api.criteria.QueryByCriteria;
import org.kuali.rice.core.framework.persistence.dao.GenericDao;
import org.kuali.rice.ken.bo.NotificationBo;
import org.kuali.rice.ken.bo.NotificationChannelBo;
import org.kuali.rice.ken.bo.NotificationContentTypeBo;
import org.kuali.rice.ken.bo.NotificationPriorityBo;
import org.kuali.rice.ken.bo.NotificationProducerBo;
import org.kuali.rice.ken.bo.NotificationRecipientBo;
import org.kuali.rice.ken.bo.NotificationSenderBo;
import org.kuali.rice.ken.service.NotificationContentTypeService;
import org.kuali.rice.krad.data.DataObjectService;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;

/**
 * A general Utility class for the Notification system.
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
public final class Util {
    private static final Logger LOG = Logger.getLogger(Util.class);
    
    public static final java.lang.String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
    public static final java.lang.String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";

    public static final NamespaceContext NOTIFICATION_NAMESPACE_CONTEXT
        = new ConfiguredNamespaceContext(Collections.singletonMap("nreq", "ns:notification/NotificationRequest"));

    private static final String ZULU_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
    private static final TimeZone ZULU_TZ = TimeZone.getTimeZone("UTC");

    private static final String CURR_TZ_FORMAT = "MM/dd/yyyy hh:mm a";
    
	private Util() {
		throw new UnsupportedOperationException("do not call");
	}

    /**
     * @return the name of the user configured to be the Notification system user
     */
    public static String getNotificationSystemUser() {
        String system_user = ConfigContext.getCurrentContextConfig().getProperty(NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_PARAM);
        if (system_user == null) {
            system_user = NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER;
        }
        return system_user;
    }

    /**
     * Parses a date/time string under XSD dateTime type syntax
     * @see #ZULU_FORMAT
     * @param dateTimeString an XSD dateTime-formatted String
     * @return a Date representing the time value of the String parameter 
     * @throws ParseException if an error occurs during parsing 
     */
    public static Date parseXSDDateTime(String dateTimeString) throws ParseException {
            return createZulu().parse(dateTimeString);
    }

    /**
     * Formats a Date into XSD dateTime format
     * @param d the date value to format
     * @return date value formatted into XSD dateTime format
     */
    public static String toXSDDateTimeString(Date d) {
        return createZulu().format(d);
    }
    
    /**
     * Returns the current date formatted for the UI
     * @return the current date formatted for the UI
     */
    public static String getCurrentDateTime() {
        return toUIDateTimeString(new Date());
    }
    
    /**
     * Returns the specified date formatted for the UI
     * @return the specified date formatted for the UI
     */
    public static String toUIDateTimeString(Date d) {
        return createCurrTz().format(d);
    }

    /**
     * Parses the string in UI date time format
     * @return the date parsed from UI date time format
     */
    public static Date parseUIDateTime(String s) throws ParseException {
        return createCurrTz().parse(s);
    }

    /**
     * Returns a compound NamespaceContext that defers to the preconfigured notification namespace context
     * first, then delegates to the document prefix/namespace definitions second.
     * @param doc the Document to use for prefix/namespace resolution
     * @return  compound NamespaceContext
     */
    public static NamespaceContext getNotificationNamespaceContext(Document doc) {
        return new CompoundNamespaceContext(NOTIFICATION_NAMESPACE_CONTEXT, new DocumentNamespaceContext(doc));
    }

    /**
     * Returns an EntityResolver to resolve XML entities (namely schema resources) in the notification system
     * @param notificationContentTypeService the NotificationContentTypeService
     * @return an EntityResolver to resolve XML entities (namely schema resources) in the notification system
     */
    public static EntityResolver getNotificationEntityResolver(NotificationContentTypeService notificationContentTypeService) {
        return new CompoundEntityResolver(new ClassLoaderEntityResolver("schema", "notification"),
                                          new ContentTypeEntityResolver(notificationContentTypeService));
    }

    /**
     * transformContent - transforms xml content in notification to a string
     * using the xsl in the datastore for a given documentType
     * @param notification
     * @return
     */
    public static String transformContent(NotificationBo notification) {
        NotificationContentTypeBo contentType = notification.getContentType();
        String xsl = contentType.getXsl();
        
        LOG.debug("xsl: "+xsl);
        
        XslSourceResolver xslresolver = new XslSourceResolver();
        //StreamSource xslsource = xslresolver.resolveXslFromFile(xslpath);
        StreamSource xslsource = xslresolver.resolveXslFromString(xsl);
        String content = notification.getContent();
        LOG.debug("xslsource:"+xslsource.toString());
        
        String contenthtml = new String();
        try {
          ContentTransformer transformer = new ContentTransformer(xslsource);
          contenthtml = transformer.transform(content);
          LOG.debug("html: "+contenthtml);
        } catch (IOException ex) {
            LOG.error("IOException transforming document",ex);
        } catch (Exception ex) {
            LOG.error("Exception transforming document",ex);
        } 
        return contenthtml;
    }

    /**
     * This method uses DOM to parse the input source of XML.
     * @param source the input source
     * @param validate whether to turn on validation
     * @param namespaceAware whether to turn on namespace awareness
     * @return Document the parsed (possibly validated) document
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static Document parse(final InputSource source, boolean validate, boolean namespaceAware, EntityResolver entityResolver) throws ParserConfigurationException, IOException, SAXException {
        // TODO: optimize this
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setValidating(validate);
        dbf.setNamespaceAware(namespaceAware);
        dbf.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
        DocumentBuilder db = dbf.newDocumentBuilder();
        if (entityResolver != null) {
            db.setEntityResolver(entityResolver);
        }
        db.setErrorHandler(new ErrorHandler() {
            public void warning(SAXParseException se) {
                LOG.warn("Warning parsing xml doc " + source, se);
            }
            public void error(SAXParseException se) throws SAXException {
                LOG.error("Error parsing xml doc " + source, se);
                throw se;
            }
            public void fatalError(SAXParseException se) throws SAXException {
                LOG.error("Fatal error parsing xml doc " + source, se);
                throw se;
            }
        });
        return db.parse(source);
    }

    /**
     * This method uses DOM to parse the input source of XML, supplying a notification-system-specific
     * entity resolver.
     * @param source the input source
     * @param validate whether to turn on validation
     * @param namespaceAware whether to turn on namespace awareness
     * @return Document the parsed (possibly validated) document
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    public static Document parseWithNotificationEntityResolver(final InputSource source, boolean validate, boolean namespaceAware, NotificationContentTypeService notificationContentTypeService) throws ParserConfigurationException, IOException, SAXException {
        return parse(source, validate, namespaceAware, getNotificationEntityResolver(notificationContentTypeService));
    }

    /**
     * Returns a node child with the specified tag name of the specified parent node,
     * or null if no such child node is found. 
     * @param parent the parent node
     * @param name the name of the child node
     * @return child node if found, null otherwise
     */
    public static Element getChildElement(Node parent, String name) {
        NodeList childList = parent.getChildNodes();
        for (int i = 0; i < childList.getLength(); i++) {
            Node node = childList.item(i);
            // we must test against NodeName, not just LocalName
            // LocalName seems to be null - I am guessing this is because
            // the DocumentBuilderFactory is not "namespace aware"
            // although I would have expected LocalName to default to
            // NodeName
            if (node.getNodeType() == Node.ELEMENT_NODE
                && (name.equals(node.getLocalName())
                   || name.equals(node.getNodeName()))) {
                return (Element) node;
            }
        }
        return null;
    }
    
    /**
     * This method will clone a given Notification object, one level deep, returning a fresh new instance 
     * without any references.
     * @param notification the object to clone
     * @return Notification a fresh instance
     */
    public static final NotificationBo cloneNotificationWithoutObjectReferences(NotificationBo notification) {
	NotificationBo clone = new NotificationBo();
	
	// handle simple data types first
        if(notification.getCreationDateTime() != null) {
            clone.setCreationDateTimeValue(new Timestamp(notification.getCreationDateTimeValue().getTime()));
        }
	if(notification.getAutoRemoveDateTime() != null) {
	    clone.setAutoRemoveDateTimeValue(new Timestamp(notification.getAutoRemoveDateTimeValue().getTime()));
	}
	clone.setContent(new String(notification.getContent()));
	clone.setDeliveryType(new String(notification.getDeliveryType()));
	if(notification.getId() != null) {
	    clone.setId(new Long(notification.getId()));
	}
	clone.setProcessingFlag(new String(notification.getProcessingFlag()));
	if(notification.getSendDateTimeValue() != null) {
	    clone.setSendDateTimeValue(new Timestamp(notification.getSendDateTimeValue().getTime()));
	}
	
        clone.setTitle(notification.getTitle());
        
	// now take care of the channel
	NotificationChannelBo channel = new NotificationChannelBo();
	channel.setId(new Long(notification.getChannel().getId()));
	channel.setName(new String(notification.getChannel().getName()));
	channel.setDescription(new String(notification.getChannel().getDescription()));
	channel.setSubscribable(new Boolean(notification.getChannel().isSubscribable()).booleanValue());
	clone.setChannel(channel);
	
	// handle the content type
	NotificationContentTypeBo contentType = new NotificationContentTypeBo();
	contentType.setId(new Long(notification.getContentType().getId()));
	contentType.setDescription(new String(notification.getContentType().getDescription()));
	contentType.setName(new String(notification.getContentType().getName()));
	contentType.setNamespace(new String(notification.getContentType().getNamespace()));
	clone.setContentType(contentType);
	
	// take care of the prioirity
	NotificationPriorityBo priority = new NotificationPriorityBo();
	priority.setDescription(new String(notification.getPriority().getDescription()));
	priority.setId(new Long(notification.getPriority().getId()));
	priority.setName(new String(notification.getPriority().getName()));
	priority.setOrder(new Integer(notification.getPriority().getOrder()));
	clone.setPriority(priority);
	
	// take care of the producer
	NotificationProducerBo producer = new NotificationProducerBo();
	producer.setDescription(new String(notification.getProducer().getDescription()));
	producer.setId(new Long(notification.getProducer().getId()));
	producer.setName(new String(notification.getProducer().getName()));
	producer.setContactInfo(new String(notification.getProducer().getContactInfo()));
	clone.setProducer(producer);
	
	// process the list of recipients now
	ArrayList<NotificationRecipientBo> recipients = new ArrayList<NotificationRecipientBo>();
	for(int i = 0; i < notification.getRecipients().size(); i++) {
	    NotificationRecipientBo recipient = notification.getRecipient(i);
	    NotificationRecipientBo cloneRecipient = new NotificationRecipientBo();
	    cloneRecipient.setRecipientId(new String(recipient.getRecipientId()));
	    cloneRecipient.setRecipientType(new String(recipient.getRecipientType()));
	    
	    recipients.add(cloneRecipient);
	}
	clone.setRecipients(recipients);
	
	// process the list of senders now
	ArrayList<NotificationSenderBo> senders = new ArrayList<NotificationSenderBo>();
	for(int i = 0; i < notification.getSenders().size(); i++) {
	    NotificationSenderBo sender = notification.getSender(i);
	    NotificationSenderBo cloneSender = new NotificationSenderBo();
	    cloneSender.setSenderName(new String(sender.getSenderName()));
	    
	    senders.add(cloneSender);
	}
	clone.setSenders(senders);
	
	return clone;
    }
    
    /**
     * This method generically retrieves a reference to foreign key objects that are part of the content, to get 
     * at the reference objects' pk fields so that those values can be used to store the notification with proper 
     * foreign key relationships in the database.
     * @param <T>
     * @param fieldName
     * @param keyName
     * @param keyValue
     * @param clazz
     * @param dataObjectService
     * @return T
     * @throws IllegalArgumentException
     */
    public static <T> T retrieveFieldReference(String fieldName, String keyName, String keyValue, Class clazz,
            DataObjectService dataObjectService, Boolean searchCurrentField) throws RuntimeException {

        LOG.debug(fieldName + " key value: " + keyValue);
        if (StringUtils.isBlank(keyValue)) {
            throw new IllegalArgumentException(fieldName + " must be specified in notification");
        }

        List<Predicate> predicates = new ArrayList<Predicate>();
        predicates.add(equal(keyName, keyValue));
        if (searchCurrentField) {
            predicates.add(equal("current", Boolean.TRUE));
        }
        QueryByCriteria.Builder criteria = QueryByCriteria.Builder.create();
        criteria.setPredicates(predicates.toArray(new Predicate[predicates.size()]));
        List<T> references = dataObjectService.findMatching(clazz, criteria.build()).getResults();

        if (references.isEmpty()) {
            throw new IllegalArgumentException(fieldName + " '" + keyValue + "' not found");
        }
        if (references.size() > 1) {
            throw new RuntimeException("More than one item found for the given value: " + keyValue);
        }

        return references.get(0);
    }

    public static <T> T retrieveFieldReference(String fieldName, String keyName, String keyValue, Class clazz, DataObjectService dataObjectService) throws RuntimeException {
        return retrieveFieldReference(fieldName, keyName, keyValue, clazz, dataObjectService, false);
    }
    /** date formats are not thread safe so creating a new one each time it is needed. */
    private static DateFormat createZulu() {
        final DateFormat df = new SimpleDateFormat(ZULU_FORMAT);
        df.setTimeZone(ZULU_TZ);
        return df;
    }

    /** date formats are not thread safe so creating a new one each time it is needed. */
    private static DateFormat createCurrTz() {
        return new SimpleDateFormat(CURR_TZ_FORMAT);
    }
}
