/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.notice.service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.PortletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

import org.jasig.portlet.notice.INotificationService;
import org.jasig.portlet.notice.NotificationResponse;
import org.jasig.portlet.notice.service.filter.FilteringNotificationServiceDecorator;
import org.jasig.portlet.notice.service.jdbc.AbstractJdbcNotificationService;
import org.jasig.portlet.notice.util.PortletXmlRoleService;
import org.jasig.portlet.notice.util.UsernameFinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.ApplicationContext;

/**
 * This class decorates and aggregates all the notification service providers. It also provides
 * caching via EHCache. Each child context (e.g. portlet) has it's own instance of this class.
 */
public final class CacheNotificationService extends AbstractNotificationService {

    // Wired by Spring
    private ApplicationContext applicationContext;
    private List<INotificationService> embeddedServices;
    private Cache cache;

    // Managed internally
    private final Map<String,INotificationService> servicesMap = new HashMap<>();
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Required
    public void setEmbeddedServices(List<INotificationService> embeddedServices) {
        this.embeddedServices = embeddedServices;
    }

    @Resource(name="notificationResponseCache")
    public void setCache(Cache cache) {
        this.cache = cache;
    }

    @PostConstruct
    public void init() {
        servicesMap.clear();  // reset the Map

        logger.info("Configuring CacheNotificationService with name='{}'", getName());

        // Load the services provided explicitly
        for (INotificationService s : embeddedServices) {
            if (servicesMap.containsKey(s.getName())) {
                // This is a configuration error
                final String msg = "Notification services names must be unique;  duplicate detected for name:  " + s.getName();
                throw new IllegalArgumentException(msg);
            }
            servicesMap.put(s.getName(), s);
            logger.info("Added configured Notification Service:  {}", s.getName());
        }

        // Discover services provided by implementors (TODO: we should move everything to discovery)
        final Map<String,AbstractJdbcNotificationService> discoverableServices =
                BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext,
                        AbstractJdbcNotificationService.class);
        if (discoverableServices.size() != 0) {
            /*
             * To match the behavior/capabilities of INotificationService beans declared in the
             * applicationContext.xml, we need to decorate these beans in a
             * FilteringNotificationServiceDecorator.  We're also obligated to provide the
             * dependencies of FilteringNotificationServiceDecorator.
             */
            final UsernameFinder usernameFinder =
                    (UsernameFinder) applicationContext.getBean("usernameFinder");
            final PortletXmlRoleService portletXmlRoleService =
                    (PortletXmlRoleService) applicationContext.getBean("portletXmlRoleService");

            for (AbstractJdbcNotificationService s : discoverableServices.values()) {
                final FilteringNotificationServiceDecorator decorator = new FilteringNotificationServiceDecorator();
                decorator.setEnclosedNotificationService(s);
                decorator.setUsernameFinder(usernameFinder);
                decorator.setPortletXmlRoleService(portletXmlRoleService);

                servicesMap.put(decorator.getName(), decorator);
                logger.info("Added discovered Notification Service:  {}", s.getName());
            }
        }
    }

    @Override
    public void invoke(ActionRequest req, ActionResponse res, boolean refresh) {

        logger.trace("Processing invoke() for user '{}' refresh={}",
                usernameFinder.findUsername(req), refresh);

        if (refresh) {
            // Make certain we have a cache MISS on next fetch()
            final String cacheKey = createServiceUserWindowSpecificCacheKey(req);
            cache.remove(cacheKey);
        }

        for (INotificationService service : servicesMap.values()) {
            service.invoke(req, res, refresh);
        }

    }

    @Override
    public void collect(EventRequest req, EventResponse res) {

        for (INotificationService service : servicesMap.values()) {
            service.collect(req, res);
        }

    }

    @Override
    public NotificationResponse fetch(PortletRequest req) {

        final String username = usernameFinder.findUsername(req);
        logger.debug("Notifications requested for user='{}' and windowId={}", username, req.getWindowID());

        CacheTuple tuple;  // Existing or new?
        final String cacheKey = createServiceUserWindowSpecificCacheKey(req);
        final Element m = cache.get(cacheKey);
        if (m != null) {
            logger.debug("Cache HIT for user='{}' and windowId={}", username, req.getWindowID());

            // We have a cached element, but it could be
            // PARTIALLY invalid; make sure it's fresh
            tuple = (CacheTuple) m.getObjectValue();
            final Map<String,NotificationResponse> iterable =
                    new HashMap<>(tuple.getResponses());  // Can't iterate & modify the same collection
            for (Map.Entry<String,NotificationResponse> entry : iterable.entrySet()) {
                final INotificationService service = servicesMap.get(entry.getKey());
                if (service == null) {
                    // This is perplexing -- should not happen
                    logger.warn("Unmatched NotificationResponse in CacheTuple;  " +
                            "service.name()='{}' and user='{}'", entry.getKey(), username);
                    tuple.getResponses().remove(entry.getKey());
                    continue;
                }
                // Refresh if needed
                if (!service.isValid(req, entry.getValue())) {
                    final NotificationResponse freshResponse = getResponseFromService(req, service);
                    tuple.getResponses().put(entry.getKey(), freshResponse);
                }
            }
        } else {
            logger.debug("Cache MISS for user='{}' and windowId={}", username, req.getWindowID());

            // For whatever reason we can't pull from cache;  we need to hit
            // the underlying data sources, then cache what we receive
            tuple = new CacheTuple();
            for(INotificationService service : servicesMap.values()) {
                final NotificationResponse nr = getResponseFromService(req, service);
                tuple.getResponses().put(service.getName(), nr);
            }
            cache.put(new Element(cacheKey, tuple));
        }

        // Construct a new NotificationResponse from constituent parts...
        NotificationResponse rslt = new NotificationResponse();
        for (Map.Entry<String,NotificationResponse> entry : tuple.getResponses().entrySet()) {
            rslt = rslt.combine(entry.getValue());
        }

        return rslt;

    }

    /*
     * Implementation
     */

    private NotificationResponse getResponseFromService(PortletRequest req, INotificationService service) {
        NotificationResponse rslt;
        try {
            rslt = service.fetch(req);
        } catch (Exception e) {
            final String msg = "Failed to invoke the specified service:  " + service.getName();
            logger.error(msg, e);
            rslt = prepareErrorResponse(getName(), msg);
        }
        return rslt;
    }

    /*
     * Nested Types
     */

    private static final class CacheTuple {

        // Instance members
        private final Map<String,NotificationResponse> responses = new HashMap<>();

        public Map<String,NotificationResponse> getResponses() {
            return this.responses;
        }

    }

}


/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.notice.service.ssp;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.ReadContext;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;

import javax.annotation.Resource;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import java.util.Map;

/**
 * This interface looks up the SSP user id by school id.  It assumes
 * that studentId is a user attribute that is available to the portlet.
 *
 * @author Josh Helmer, jhelmer.unicon.net
 */
public class SSPSchoolIdPersonLookup implements ISSPPersonLookup {
    private Logger log = LoggerFactory.getLogger(getClass());

    private static final String USERNAME_ATTRIBUTE = "user.login.id";
    private static final String SUCCESS_QUERY = "$.success";
    private static final String RESULTS_QUERY = "$.results";
    private static final String STUDENT_ID_QUERY = "$.rows[0].id";

    private String personSearchURL = "/api/1/person/directoryperson/search?limit=2&schoolId={schoolId}&start=0";
    private ISSPApi sspApi;
    private Cache cache;

    @Resource(name="StudentSuccessPlanService.schoolIdToPersonIdCache")
    public void setCache(final Cache cache) {
        this.cache = cache;
    }

    @Autowired
    public void setSspApi(ISSPApi sspApi) {
        this.sspApi = sspApi;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String lookupPersonId(PortletRequest request) {
        String studentId = getSchoolId(request);
        if (StringUtils.isBlank(studentId)) {
            return null;
        }

        Element element = cache.get(studentId);
        if (element != null) {
            return (String)element.getObjectValue();
        }

        String url = getPersonSearchURL();
        SSPApiRequest sspReq = new SSPApiRequest(url, String.class)
                .addUriParameter("schoolId", studentId);

        try {
            ResponseEntity<String> response = sspApi.doRequest(sspReq);
            String userId = extractUserId(studentId, response);

            Element cacheElement = new Element(studentId, userId);
            cache.put(cacheElement);

            return userId;

        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * Get the schoolId value from the request.
     *
     * @param request the portlet request
     * @return the school id.   May return null if the school id is not available
     */
    private String getSchoolId(PortletRequest request) {
        PortletPreferences prefs = request.getPreferences();
        String schoolIdAttributeName = prefs.getValue("SSPTaskNotificationService.schoolIdAttribute", "schoolId");

        Map<String, String> userInfo = (Map<String, String>)request.getAttribute(PortletRequest.USER_INFO);
        String studentId = userInfo.get(schoolIdAttributeName);
        if (!StringUtils.isEmpty(studentId)) {
            return studentId;
        }

        // if not found, fall back to username.
        studentId = userInfo.get(USERNAME_ATTRIBUTE);

        return studentId;
    }

    /**
     * Parse the person lookup response from SSP.
     *
     * @param studentId the uPortal studentid
     * @param response the SSP response
     * @return the SSP id if available, else null
     */
    private String extractUserId(String studentId, ResponseEntity<String> response) {
        Configuration config = Configuration.builder().options(
                Option.DEFAULT_PATH_LEAF_TO_NULL
        ).build();
        ReadContext readContext = JsonPath
                .using(config)
                .parse(response.getBody());

        String success = readContext.read(SUCCESS_QUERY);
        // SSP passes this as a string...
        if (!"true".equalsIgnoreCase(success)) {
            return null;
        }

        int count = readContext.read(RESULTS_QUERY, Integer.class);
        if (count != 1) {
            // couldn't find a single unique result.  Bail now...
            log.warn("Expected a single unique result for " + studentId + ".  Found " + count);
            return null;
        }

        String id = readContext.read(STUDENT_ID_QUERY);
        return id;
    }

    private String getPersonSearchURL() {
        return personSearchURL;
    }
}


/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.notice.service.event;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.servlet.http.HttpServletRequest;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

import org.jasig.portlet.notice.INotificationService;
import org.jasig.portlet.notice.NotificationConstants;
import org.jasig.portlet.notice.NotificationQuery;
import org.jasig.portlet.notice.NotificationResponse;
import org.jasig.portlet.notice.NotificationResult;
import org.jasig.portlet.notice.controller.NotificationLifecycleController;
import org.jasig.portlet.notice.service.AbstractNotificationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Concrete {@link INotificationService} class that gathers notifications from other portlets based
 * on portlet events.
 *
 * @deprecated Prefer interactions that are not based on the Portlet API
 */
@Deprecated
public final class PortletEventNotificationService extends AbstractNotificationService {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private Cache cache;

    @Resource(name="PortletEventNotificationService.responseCache")
    public void setCache(Cache cache) {
        this.cache = cache;
    }

    @Override
    public void invoke(final ActionRequest req, final ActionResponse res, final boolean refresh) {

        // Since this behavior is potentially a great deal of work for the
        // portal, there is a portlet preference required to turn it on -- even
        // if the bean is configured in the context.
        final PortletPreferences prefs = req.getPreferences();
        final boolean doEvents = Boolean.parseBoolean(prefs.getValue(NotificationLifecycleController.DO_EVENTS_PREFERENCE, "false"));
        if (doEvents) {

            // Find the cached notifications, if any
            final String cacheKey = createServiceUserWindowSpecificCacheKey(req);

            // Send a request event?
            boolean sendRequestEvent = false;  // default
            if (refresh) {
                // Yes!
                cache.remove(cacheKey);
                sendRequestEvent = true;
            } else {
                // Not unless...
                final Element m = cache.get(cacheKey);
                if (m == null) {
                    // Yes!
                    sendRequestEvent = true;
                }
            }

            if (sendRequestEvent) {
                if (logger.isDebugEnabled()) {
                    logger.debug("REQUESTING Notifications events for user='"
                                        + usernameFinder.findUsername(req)
                                        + "' and windowId=" + req.getWindowID());
                }
                NotificationQuery query = new NotificationQuery();
                query.setQueryWindowId(req.getWindowID());
                res.setEvent(NotificationConstants.NOTIFICATION_QUERY_QNAME, query);
            }

        }

    }

    @SuppressWarnings("unchecked")
    @Override
    public void collect(final EventRequest req, final EventResponse res) {

        if (logger.isDebugEnabled()) {
            logger.debug("RECEIVING Notifications events for user='"
                                + usernameFinder.findUsername(req)
                                + "' and windowId=" + req.getWindowID());
        }

        // Implements Serializable
        HashMap<String,NotificationResponse> responses;

        final String cacheKey = createServiceUserWindowSpecificCacheKey(req);

        final Element m = cache.get(cacheKey);
        if (m == null) {
            // Start fresh...
            responses = new HashMap<String,NotificationResponse>();
        } else {
            responses = (HashMap<String,NotificationResponse>) m.getObjectValue();
        }

        final NotificationResult notificationResult = (NotificationResult) req.getEvent().getValue();
        responses.put(notificationResult.getResultWindowId(), notificationResult.getNotificationResponse());
        cache.put(new Element(cacheKey, responses));

    }

    @Override
    public NotificationResponse fetch(final PortletRequest req) {

        NotificationResponse rslt = NotificationResponse.EMPTY_RESPONSE;  // default is empty

        final String cacheKey = createServiceUserWindowSpecificCacheKey(req);
        final Element m = cache.get(cacheKey);
        if (m != null) {
            @SuppressWarnings("unchecked")
            final Map<String,NotificationResponse> map = (Map<String,NotificationResponse>) m.getObjectValue();
            for (NotificationResponse response : map.values()) {
                rslt = rslt.combine(response);
            }
        }

        return rslt;

    }

    /**
     * This {@link INotificationService} is fundamentally about Java Portlets, and there is nothing
     * it can offer to this overload of fetch.
     *
     * @since 4.0
     */
    @Override
    public NotificationResponse fetch(HttpServletRequest request) {
        logger.trace("{} invoked for user '{}', but there is nothing to do",
                getClass().getSimpleName(), usernameFinder.findUsername(request));
        return NotificationResponse.EMPTY_RESPONSE;
    }

}


/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.notice.action.hide;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletPreferences;
import javax.portlet.PortletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.jasig.portlet.notice.NotificationAction;
import org.jasig.portlet.notice.NotificationEntry;
import org.jasig.portlet.notice.NotificationState;
import org.jasig.portlet.notice.rest.EventDTO;
import org.jasig.portlet.notice.util.JpaServices;
import org.jasig.portlet.notice.util.SpringContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HideAction extends NotificationAction {

    public static final String HIDE_DURATION_HOURS_ATTRIBUTE_NAME = HideAction.class.getName()
                                                    + ".HIDE_DURATION_HOURS_ATTRIBUTE_NAME";

    /**
     * This INSTANCE is only for convenience -- HideAction is not a singleton.
     * There may be situations where de-serialization will create additional
     * instances, and that's okay.
     */
    public static final HideAction INSTANCE = new HideAction();

    private static final long MILLIS_IN_ONE_HOUR = 60L * 60L * 1000L;

    private static final long serialVersionUID = 1L;

    private final Logger logger = LoggerFactory.getLogger(HideAction.class);

    /**
     * Must remain public, no-arg for de-serialization.
     */
    public HideAction() {
        // Set a default label;  most use cases will use the setter and override
        setLabel("HIDE");
    }

    /**
     * Invoking a HideAction toggles it.
     */
    @Override
    public void invoke(final ActionRequest req, final ActionResponse res) throws IOException {

        final NotificationEntry entry = getTarget();

        /*
         * The HideAction works like a toggle
         */
        if (!isEntrySnoozed(entry, req)) {
            // Hide it...
            hide(entry, req);
        } else {
            // Un-hide it...
            unhide(entry, req);
        }

    }

    @Override
    public void invoke(HttpServletRequest request, HttpServletResponse response) {
        throw new UnsupportedOperationException("TODO:  Implement!");
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        final String id = getId();
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        // At present, any instance HideAction is equal to another
        return true;
    }

    /* package-private */ long calculateHideDurationMillis(final NotificationEntry entry, final PortletRequest req) {

        /*
         * A notification may specify it's own duration as an attribute;
         * if it doesn't, there is a default from the (portlet) publication record.
         */

        final PortletPreferences prefs = req.getPreferences();

        // This is the default
        String hideDurationHours = prefs.getValue(
                HideNotificationServiceDecorator.HIDE_DURATION_HOURS_PREFERENCE, null);

        // Duration specified on the entry itself will trump
        final Map<String,List<String>> attributes = entry.getAttributesMap();
        if (attributes.containsKey(HIDE_DURATION_HOURS_ATTRIBUTE_NAME)) {
            final List<String> values = attributes.get(HIDE_DURATION_HOURS_ATTRIBUTE_NAME);
            if (values.size() != 0) {
                hideDurationHours = values.get(0);  // First is the only one that matters
            }
        }

        final long rslt = hideDurationHours != null
                ? Long.parseLong(hideDurationHours) * MILLIS_IN_ONE_HOUR
                : HideNotificationServiceDecorator.HIDE_DURATION_NONE;
        logger.debug("Calculated calculateHideDurationMillis={} for entry with id='{}', title='{}'",
                                rslt, entry.getId(), entry.getTitle());

        return rslt;

    }

    /* package-private */ boolean isEntrySnoozed(NotificationEntry entry, PortletRequest req) {

        // An id is required for hide behavior
        if (StringUtils.isBlank(entry.getId())) {
            return false;
        }

        boolean rslt = false;  // default (clearly)

        final long snoozeDurationMillis = calculateHideDurationMillis(entry, req);

        // An entry with a negative snooze duration cannot be snoozed
        if (snoozeDurationMillis > HideNotificationServiceDecorator.HIDE_DURATION_NONE) {
            final JpaServices jpaServices = (JpaServices) SpringContext.getApplicationContext().getBean("jpaServices");
            final List<EventDTO> history = jpaServices.getHistory(entry, req.getRemoteUser());
            logger.debug("List<EventDTO> within getNotificationsBySourceAndCustomAttribute contains {} elements", history.size());

            // Review the history...
            for (EventDTO event : history) {
                switch (event.getState()) {
                    case SNOOZED:
                        logger.debug("Found a SNOOZED event:  {}", event);
                        // Nice, but it only counts if it isn't expired...
                        if (event.getTimestamp().getTime() + snoozeDurationMillis > System.currentTimeMillis()) {
                            rslt = true;
                        }
                        break;
                    case ISSUED:
                        logger.debug("Found an ISSUED event:  {}", event);
                        // Re-issuing a notification un-snoozes it...
                        rslt = false;
                        break;
                    default:
                        // We don't care about any other events in the SNOOZED evaluation...
                        break;
                }
            }
        }

        logger.debug("Returning SNOOZED='{}' for the following notification:  {}", rslt, entry);
        return rslt;

    }

    /*
     * Implementation
     */

    private void hide(NotificationEntry entry, ActionRequest req) {
        final JpaServices jpaServices = (JpaServices) SpringContext.getApplicationContext().getBean("jpaServices");
        jpaServices.applyState(entry, req.getRemoteUser(), NotificationState.SNOOZED);
    }

    private void unhide(NotificationEntry entry, ActionRequest req) {
        final JpaServices jpaServices = (JpaServices) SpringContext.getApplicationContext().getBean("jpaServices");
        // Re-issuing trumps the snooze
        jpaServices.applyState(entry, req.getRemoteUser(), NotificationState.ISSUED);
    }

}


/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 the following location:
 *
 *   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.jasig.portlet.notice;

import java.io.Serializable;
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 javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.apache.commons.lang.builder.ToStringBuilder;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

/**
 * This class represents a single notification.  It defines a handful of
 * strongly-typed members, plus a few open-ended collections.  These collections
 * are for attributes, actions, and states.
 * Strongly-typed items are chosen primarily because they require
 * framework-level special handling.  The open-ended collections make the
 * platform extensible.  Please add additional members only as a last
 * resort.
 *
 *
The {@link NotificationCategory}
 * class contains all the entries for the same category title.
 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true) // When deserializing, ignore extra fields
@XmlAccessorType(XmlAccessType.FIELD)
public class NotificationEntry implements Serializable, Cloneable {

    /**
     * The priority field of a {@link NotificationEntry} will contain this value when
     * the priority is unspecified by the source.
     *
     * @since 3.1
     */
    public static final int PRIORITY_UNSPECIFIED = 0;

    private static final long serialVersionUID = 1L;

    /*
     * Strongly-typed members
     */

    private String    source;
    private String    id;
    private String    title;
    private String    url;
    private String    linkText;
    private int       priority;
    private Date      dueDate;
    private String    image;
    private String    body;

    /*
     * Weakly-typed, open-ended attributes collection
     */
    private List<NotificationAttribute> attributes = Collections.emptyList();

    /*
     * Operations that a user may perform on this notification
     */
    private List<NotificationAction> availableActions = Collections.emptyList();

    /*
     * Representation of where this notification is in applicable workflow(s).
     */
    private Map<NotificationState,Date> states = Collections.emptyMap();

    /**
     * Provides the human-readable name for the source of this notification.
     * E.g. 'Office of the Registrar'
     */
    public String getSource() {
        return source;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public String getId() {
        return id;
    }

    /**
     * Sets the unique identifier for this notification.  Ids are not required
     * except for notifications that support actions.  The
     * value of the id field can be any javascript-safe String, but must be
     * unique within a user's collection of notifications.
     * UUID.randomUUID() could be a good way to generate them.
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * Required one-liner version of this notification.  E.g. 'You have 3
     * overdue library books.'
     */
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    /**
     * Optional web address where the user can deal with or learn more about the
     * details of this notification.
     */
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * Optional text of the anchor tag linking to url.
     */
    public String getLinkText() {
        return linkText;
    }

    public void setLinkText(String linkText) {
        this.linkText = linkText;
    }

    /**
     * Optional numeric representation of the relative importance of the
     * notification.  Vales range from 1 (most important) to 5 (least important).
     */
    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    /**
     * Optional date/time indicating by when the action described in this
     * notification must be completed.
     */
    @JsonSerialize(using=JsonDateSerializer.class)
    public Date getDueDate() {
        return dueDate;
    }

    @JsonDeserialize(using=JsonDateDeserializer.class)
    public void setDueDate(Date dueDate) {
        this.dueDate = dueDate;
    }

    /**
     * Optional URL of an image file associated with this notification.  Might
     * be the logo of the source system or something more specific.
     */
    public String getImage() {
        return image;
    }

    public void setImage(String imageUrl) {
        this.image = imageUrl;
    }

    /**
     * Optional body text for this notification.  Supports HTML tags.
     */
    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    /**
     * Open-ended collection of notification metadata.  The presumption is that
     * attributes will be visible to users.
     */
    @JsonSerialize(using=JsonAttributesSerializer.class)
    public List<NotificationAttribute> getAttributes() {
        return Collections.unmodifiableList(attributes);
    }

    @JsonDeserialize(using=JsonAttributesDeserializer.class)
    public void setAttributes(List<NotificationAttribute> attributes) {
        this.attributes = new ArrayList<>(attributes);  // defensive copy
    }

    /**
     * Convenience method for obtaining the attributes in a more usable collection.
     */
    @JsonIgnore
    public Map<String,List<String>> getAttributesMap() {
        Map<String,List<String>> rslt = new HashMap<>();
        for (NotificationAttribute a : attributes) {
            rslt.put(a.getName(), a.getValues());
        }
        return rslt;
    }

    /**
     * Open-ended collection of behaviors the user (recipient) can invoke upon
     * the notification.  E.g. 'hide' or 'mark as done'.  Different Notification
     * UIs may support different behaviors (viz. not all behaviors will be
     * supported by all view JSPs).
     */
    public List<NotificationAction> getAvailableActions() {
        return Collections.unmodifiableList(availableActions);
    }

    public void setAvailableActions(List<NotificationAction> availableActions) {
        this.availableActions = new ArrayList<>();  // defensive copy
        for (NotificationAction action : availableActions) {
            // We must make ourself the target of any
            // action at the time it becomes attached
            action.setTarget(this);
            this.availableActions.add(action);
        }
    }

    /**
     * Open-ended Map of states that currently apply to this notification.  Keys
     * are the states themselves, while values provide a way to indicate the
     * moment when the state was applied.  Data sources may leave values
     * null.
     *
     * @since 2.2
     */
    public Map<NotificationState,Date> getStates() {
        return Collections.unmodifiableMap(states);
    }

    /**
     * @since 2.2
     */
    public void setStates(Map<NotificationState,Date> states) {
        this.states = new HashMap<>(states);  // defensive copy
    }

    /**
     * Implements deep-copy clone.
     *
     * @throws CloneNotSupportedException Not really, but it's on the method
     * signature we're overriding.
     */
    @Override
    public Object clone() throws CloneNotSupportedException {

        // Start with superclass impl (handles immutables and primitives)
        final NotificationEntry rslt = (NotificationEntry) super.clone();

        // Adjust to satisfy deep-copy strategy
        List<NotificationAttribute> atrList = new ArrayList<>(attributes.size());
        for (NotificationAttribute attr : attributes) {
            atrList.add((NotificationAttribute) attr.clone());
        }
        rslt.setAttributes(atrList);
        List<NotificationAction> actList = new ArrayList<>(availableActions.size());
        for (NotificationAction action : availableActions) {
            actList.add((NotificationAction) action.clone());
        }
        rslt.setAvailableActions(actList);

        return rslt;

    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

}


/*
 * Copyright (C) 2011-2015, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.geometry;

import georegression.struct.GeoTuple2D_F64;
import georegression.struct.GeoTuple3D_F64;
import georegression.struct.point.Vector3D_F64;

/**
 *
 *
 */
public class UtilTrig_F64 {

	public static double distance( double x0, double y0, double x1, double y1 ) {
		double dx = x1 - x0;
		double dy = y1 - y0;

		return Math.sqrt( dx * dx + dy * dy );
	}

	public static double distanceSq( double x0, double y0, double x1, double y1 ) {
		double dx = x1 - x0;
		double dy = y1 - y0;

		return dx * dx + dy * dy;
	}

	public static Vector3D_F64 cross( Vector3D_F64 a, Vector3D_F64 b ) {
		double x = a.getY() * b.getZ() - a.getZ() * b.getY();
		double y = a.getZ() * b.getX() - a.getX() * b.getZ();
		double z = a.getX() * b.getY() - a.getY() * b.getX();

		return new Vector3D_F64( x, y, z );
	}

	public static double dot( double vx_a, double vy_a,
							  double vx_b, double vy_b ) {
		return vx_a * vx_b + vy_a * vy_b;
	}

	public static double dot( GeoTuple2D_F64 a , GeoTuple2D_F64 b ) {
		return a.x*b.x + a.y*b.y;
	}

	public static double dot( GeoTuple3D_F64 a , GeoTuple3D_F64 b ) {
		return a.x*b.x + a.y*b.y + a.z*b.z;
	}

	public static double acuteAngle( GeoTuple3D_F64 a , GeoTuple3D_F64 b ) {
		double dot = a.x*b.x + a.y*b.y + a.z*b.z;
		double bottom = a.norm()*b.norm();
		return Math.acos(dot/bottom);
	}

	public static double acuteAngle( double vx_a, double vy_a,
									 double vx_b, double vy_b ) {
		double r_a = Math.sqrt( vx_a * vx_a + vy_a * vy_a );
		double r_b = Math.sqrt( vx_b * vx_b + vy_b * vy_b );

		return Math.acos( ( vx_a * vx_b + vy_a * vy_b ) / ( r_a * r_b ) );
	}

	/**
	 * Normalizes the point such that the Frobenius norm is 1.
	 *
	 * @param p
	 */
	public static void normalize( GeoTuple3D_F64 p ) {
		double n = p.norm();
		p.x /= n;
		p.y /= n;
		p.z /= n;
	}
}


/*
 * Copyright (C) 2011-2015, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.metric;

import georegression.struct.point.Point3D_I32;
import georegression.struct.shapes.Box3D_I32;

/**
 * @author Peter Abeles
 */
public class Intersection3D_I32 {
	/**
	 * Returns true if the point is contained inside the box. The point is considered to be inside the box
	 * if the following test passes for each dimension.  box.p0.x ≤ point.x {@code <} box.p1.x + box.lengthX
	 *
	 * @param box Box
	 * @param point Point which is tested to see if it is inside the box
	 * @return true for inside and false for not
	 */
	public static boolean contained( Box3D_I32 box , Point3D_I32 point ) {

		return( box.p0.x <= point.x && point.x < box.p1.x &&
				box.p0.y <= point.y && point.y < box.p1.y &&
				box.p0.z <= point.z && point.z < box.p1.z );
	}

	/**
	 * Returns true if boxB is contained inside of or is identical to boxA.
	 *
	 * @param boxA Box
	 * @param boxB Box which is being tested to see if it is inside of boxA
	 * @return true if inside/identical or false if outside
	 */
	public static boolean contained( Box3D_I32 boxA , Box3D_I32 boxB ) {
		return( boxA.p0.x <= boxB.p0.x && boxA.p1.x >= boxB.p1.x &&
				boxA.p0.y <= boxB.p0.y && boxA.p1.y >= boxB.p1.y &&
				boxA.p0.z <= boxB.p0.z && boxA.p1.z >= boxB.p1.z );
	}

	/**
	 * Returns true if the two boxes intersect each other. p0 is inclusive and p1 is exclusive.
	 * So if the p0 edge and p1 edge overlap perfectly there is no intersection.
	 *
	 * @param boxA Box
	 * @param boxB Box
	 * @return true for intersection and false if no intersection
	 */
	public static boolean intersect( Box3D_I32 boxA , Box3D_I32 boxB ) {
		return( intersect(boxA.p0.x , boxB.p0.x , boxA.p1.x , boxB.p1.x ) &&
				intersect(boxA.p0.y , boxB.p0.y , boxA.p1.y , boxB.p1.y ) &&
				intersect(boxA.p0.z , boxB.p0.z , boxA.p1.z , boxB.p1.z ) );
	}

	protected static boolean intersect( int a0 , int b0 , int a1, int b1 ) {
		if( a0 <= b0 ) {
			return b0 < a1;
		} else {
			return a0 < b1;
		}
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.metric.alg;

import georegression.geometry.GeometryMath_F64;
import georegression.struct.point.Point3D_F64;
import georegression.struct.point.Vector3D_F64;

/**
 *
Computes the closest point and distance between a triangle in 3D and the closest point on a triangle.

 *
 *
David Eberly, "Distance Between Point and Triangle in 3D", Geometric Tools, LLC, 1999

 *
 * @author Peter Abeles
 */
public class DistancePointTriangle3D_F64 {

	// description of triangle
	private Point3D_F64 B = new Point3D_F64();
	private Vector3D_F64 E0 = new Vector3D_F64();
	private Vector3D_F64 E1 = new Vector3D_F64();

	// storage for cross product of E0 and E1
	private Vector3D_F64 N = new Vector3D_F64();

	// coefficients for triangle.  0 <= s,t <= 1 and s+t <= 1
	// any point can be uniquely described using s and t
	// T(s,t) = B + s*E0 + t*S1
	private double s, t;

	// storage for B-P
	private Vector3D_F64 D = new Vector3D_F64();

	private double a,b,c,d,e;

	public void setTriangle(Point3D_F64 p0, Point3D_F64 p1, Point3D_F64 p2) {

		// convert triangle into new format
		B.set(p0);
		GeometryMath_F64.sub(p1, p0, E0);
		GeometryMath_F64.sub(p2, p0, E1);
	}

	/**
	 * Find the closest point on the triangle to P.
	 * @param P Input.  The point for which the closest point is to be found.
	 * @param closestPt Output.  The found closest point.
	 */
	public void closestPoint(Point3D_F64 P , Point3D_F64 closestPt ) {

		// D = B-P
		GeometryMath_F64.sub(B, P, D);

		a = E0.dot(E0);
		b = E0.dot(E1);
		c = E1.dot(E1);
		d = E0.dot(D);
		e = E1.dot(D);

		double det = a * c - b * b;
		s = b * e - c * d;
		t = b * d - a * e;

		if (s + t <= det) {
			if (s < 0) {
				if (t < 0) {
					region4();
				} else {
					region3();
				}
			} else if (t < 0) {
				region5();
			} else {
				region0(det);
			}
		} else {
			if (s < 0) {
				region2();
			} else if (t < 0) {
				region6();
			} else {
				region1();
			}
		}

		closestPt.x = B.x + s*E0.x  + t*E1.x;
		closestPt.y = B.y + s*E0.y  + t*E1.y;
		closestPt.z = B.z + s*E0.z  + t*E1.z;
	}

	/**
	 * Returns the signed of the vector.  If its "in front" it will be positive and negative if "behind".  In front
	 * is defined as being on the same side as the cross product of p2-p0 and p1-p0.
	 *
	 * @param P Point for which the sign is determined
	 */
	public double sign( Point3D_F64 P ) {

		GeometryMath_F64.cross(E1,E0,N);

		// dot product of
		double d = N.x*(P.x-B.x) + N.y*(P.y-B.y) + N.z*(P.z-B.z);
		return Math.signum(d);
	}

	protected void region0(double det) {
		double invDet = 1.0 / det;
		s *= invDet;
		t *= invDet;
	}

	protected void region1() {
		double numer = c + e - b - d;
		if (numer <= 0) {
			s = 0;
		} else {
			double denom = a - 2 * b + c;
			s = (numer >= denom) ? 1 : numer / denom;
		}
		t = 1 - s;
	}

	protected void region2() {
		double tmp0 = b + d;
		double tmp1 = c + e;
		if( tmp1 > tmp0 ) {
			double numer = tmp1 - tmp0;
			double denom = a - 2 * b + c;
			s = numer <= denom ? 1 : numer/denom;
			t = 1 - s;
		} else {
			s = 0;
			t = tmp1 <= 0 ? 1 : (e >= 0 ? 0 : -e/c);
		}
	}

	protected void region3() {
		s = 0;
		t = e >= 0 ? 0 : (-e >= c ? 1 : -e/c);
	}

	protected void region4() {
		if (d < 0) {
			t = 0;
			s = -d >= a ? 1 : -d/a;
		}else{
			s = 0;
			t = e >= 0 ? 0 : (-e >= c ? 1 : -e/c);
		}
	}

	protected void region5() {
		t = 0;
		s = d >= 0 ? 0 : (-d >= a ? 1 : -d/a );
	}

	protected void region6() {
		double tmp0 = b + e;
		double tmp1 = a + d;
		if (tmp1 > tmp0) {
			double numer = tmp1 - tmp0;
			double denom = a - 2 * b + c;
			t = numer >= denom ? 1 : numer/denom;
			s = 1 - t;
		}else{
			t = 0;
			s = tmp1 <= 0 ? 1 : (d >= 0 ? 0 : -d/a);
		}
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.struct.se;

import georegression.geometry.ConvertRotation3D_F64;
import georegression.geometry.GeometryMath_F64;
import georegression.struct.EulerType;
import georegression.struct.GeoTuple2D_F64;
import georegression.struct.GeoTuple3D_F64;
import georegression.struct.RotationType;
import georegression.struct.point.Point3D_F64;
import georegression.struct.point.Vector3D_F64;
import georegression.transform.se.SePointOps_F64;
import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.row.CommonOps_DDRM;

import javax.annotation.Nullable;

/**
 * A coordinate system transform composed of a rotation and translation.  This transform is
 * a rigid body transform and is a member of the special euclidean group.
 *
 * @author Peter Abeles
 */
public class Se3_F64 implements SpecialEuclidean<Se3_F64> {

	// serialization version
	public static final long serialVersionUID = 1L;

	// rotation matrix
	public DMatrixRMaj R;
	// translation vector
	public Vector3D_F64 T;

	/**
	 * Creates a new transform that does nothing.
	 */
	public Se3_F64() {
		R = CommonOps_DDRM.identity( 3 );
		T = new Vector3D_F64();
	}

	/**
	 * Initializes the transform with the provided rotation and translation.
	 *
	 * @param R Rotation matrix.
	 * @param T Translation.
	 */
	public Se3_F64( DMatrixRMaj R, Vector3D_F64 T ) {
		this( R, T, false );
	}

	/**
	 * Initializes the Se3_F64 with the rotation matrix and translation vector.  If assign
	 * is true the reference to the provided parameters is saved, otherwise a copy is made.
	 *
	 * @param R	  Rotation matrix.
	 * @param T	  Translation.
	 * @param assign If a reference is saved (true) or a copy made (false).
	 */
	public Se3_F64( DMatrixRMaj R, Vector3D_F64 T, boolean assign ) {
		if( assign ) {
			this.R = R;
			this.T = T;
		} else {
			this.R = R.copy();
			this.T = T.copy();
		}
	}

	/**
	 * Set's 'this' Se3_F64 to be identical to the provided transform.
	 *
	 * @param se The transform that is being copied.
	 */
	public void set( Se3_F64 se ) {
		R.set( se.getR() );
		T.set( se.getT() );
	}

	/**
	 * Sets the rotation to R.
	 *
	 * @param R New rotation.
	 */
	public void setRotation( DMatrixRMaj R ) {
		this.R.set( R );
	}

	/**
	 * Sets the translation to T
	 * @param T New translation
	 */
	public void setTranslation( Vector3D_F64 T ) {
		this.T.set( T );
	}

	/**
	 * Sets the translation to (x,y,z)
	 * @param x x component of translation
	 * @param y y component of translation
	 * @param z z component of translation
	 */
	public void setTranslation( double x, double y, double z ) {
		this.T.set( x, y, z );
	}

	/**
	 * Returns the rotation matrix
	 * @return rotation matrix
	 */
	public DMatrixRMaj getRotation() {
		return R;
	}

	/**
	 * Returns the translation vector
	 * @return translation vector
	 */
	public Vector3D_F64 getTranslation() {
		return T;
	}

	public DMatrixRMaj getR() {
		return R;
	}

	public Vector3D_F64 getT() {
		return T;
	}

	public double getX() {
		return T.getX();
	}

	public double getY() {
		return T.getY();
	}

	public double getZ() {
		return T.getZ();
	}

	@Override
	public int getDimension() {
		return 3;
	}

	@Override
	public Se3_F64 createInstance() {
		return new Se3_F64();
	}

	@Override
	public Se3_F64 concat( Se3_F64 second, Se3_F64 result ) {
		if( result == null )
			result = new Se3_F64();

		CommonOps_DDRM.mult( second.getR(), getR(), result.getR() );
		GeometryMath_F64.mult( second.getR(), getT(), result.getT() );
		GeometryMath_F64.add( second.getT(), result.getT(), result.getT() );

		return result;
	}

	@Override
	public Se3_F64 invert( Se3_F64 inverse ) {

		if( inverse == null )
			inverse = new Se3_F64();

		// To derive the inverse transform solve for P
		// R*P+T = P'
		// P = R^T*P' - R^T*T

		// -R^T*T
		GeometryMath_F64.multTran( R, T, inverse.T );
		GeometryMath_F64.changeSign( inverse.T );

		// R^T
		CommonOps_DDRM.transpose( R, inverse.R );

		return inverse;
	}

	@Override
	public void reset() {
		CommonOps_DDRM.setIdentity( R );
		T.set( 0, 0, 0 );
	}

	/**
	 * Fully specify the transform using Euler angles
	 */
	public void set(double x , double y , double z , EulerType type , double rotA , double rotB , double rotC ) {
		T.set(x,y,z);
		ConvertRotation3D_F64.eulerToMatrix(type,rotA,rotB,rotC,R);
	}

	/**
	 * Fully specifies the transform using Rodrigues (axis angle) or Quaternions.  If Rodrigues then A=axisX, B=axisY,
	 * C=axisZ, D=theta. If Quaternion then A=w, B=x, C=y, D=z.
	 */
	public void set(double x , double y , double z , RotationType type , double A , double B , double C, double D ) {
		T.set(x,y,z);
		switch( type ) {
			case RODRIGUES:
				ConvertRotation3D_F64.rodriguesToMatrix(A,B,C,D,R);
				break;

			case QUATERNION:
				ConvertRotation3D_F64.quaternionToMatrix(A,B,C,D,R);
				break;

			default:
				throw new IllegalArgumentException("Type is not supported. "+type);
		}
	}

	/**
	 * Applies the transform to the src point and stores the result in dst. src and dst can be the same instance
	 *
	 * @see SePointOps_F64#transform(Se3_F64, Point3D_F64, Point3D_F64)
	 *
	 * @param src Input
	 * @param dst Output
	 * @return Output
	 */
	public Point3D_F64 transform(Point3D_F64 src, @Nullable Point3D_F64 dst ) {
		return SePointOps_F64.transform(this,src,dst);
	}

	/**
	 * Applies the reverse transform to the src point and stores the result in dst. src and dst can be the same instance
	 *
	 * @see SePointOps_F64#transformReverse(Se3_F64, Point3D_F64, Point3D_F64)
	 *
	 * @param src Input
	 * @param dst Output
	 * @return Output
	 */
	public Point3D_F64 transformReverse(Point3D_F64 src, @Nullable Point3D_F64 dst ) {
		return SePointOps_F64.transformReverse(this,src,dst);
	}

	/**
	 * Applies the rotation to the src vector and stores the result in dst. src and dst can be the same instance
	 *
	 * @see GeometryMath_F64#mult(DMatrixRMaj, GeoTuple2D_F64, GeoTuple2D_F64)
	 *
	 * @param src Input
	 * @param dst Output
	 */
	public Vector3D_F64 transform(Vector3D_F64 src, @Nullable Vector3D_F64 dst ) {
		return GeometryMath_F64.mult(R,src,dst);
	}

	/**
	 * Applies the reverse rotation to the src vector and stores the result in dst. src and dst can be the same instance
	 *
	 * @see GeometryMath_F64#multTran(DMatrixRMaj, GeoTuple3D_F64, GeoTuple3D_F64)
	 *
	 * @param src Input
	 * @param dst Output
	 */
	public Vector3D_F64 transformReverse(Vector3D_F64 src, @Nullable Vector3D_F64 dst ) {
		return GeometryMath_F64.multTran(R,src,dst);
	}

	public Se3_F64 copy() {
		Se3_F64 ret = new Se3_F64();
		ret.set( this );

		return ret;
	}

	public String toString() {
		String ret = "Se3_F64: T = "+T.toString()+"\n";
		ret += R;

		return ret;
	}

	public void print() {
		System.out.println(this);
	}
}


/*
 * Copyright (C) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.geometry;

import georegression.geometry.algs.AndrewMonotoneConvexHull_F64;
import georegression.metric.Distance2D_F64;
import georegression.struct.line.LineSegment2D_F64;
import georegression.struct.point.Point2D_F64;
import georegression.struct.shapes.Polygon2D_F64;
import georegression.struct.shapes.Quadrilateral_F64;
import georegression.struct.shapes.Rectangle2D_F64;
import georegression.struct.shapes.RectangleLength2D_I32;

import java.util.List;

/**
 * Various functions related to polygons.
 *
 * @author Peter Abeles
 */
public class UtilPolygons2D_F64 {

	/**
	 * Determines if the polugon is convex or concave.
	 *
	 * @param poly Polygon
	 * @return true if convex and false if concave
	 */
	public static boolean isConvex( Polygon2D_F64 poly ) {
		// if the cross product of all consecutive triples is positive or negative then it is convex

		final int N = poly.size();
		int numPositive = 0;
		for (int i = 0; i < N; i++) {
			int j = (i+1)%N;
			int k = (i+2)%N;

			Point2D_F64 a = poly.vertexes.data[i];
			Point2D_F64 b = poly.vertexes.data[j];
			Point2D_F64 c = poly.vertexes.data[k];

			double dx0 = a.x-b.x;
			double dy0 = a.y-b.y;

			double dx1 = c.x-b.x;
			double dy1 = c.y-b.y;

			double z = dx0 * dy1 - dy0 * dx1;
			if( z > 0 )
				numPositive++;
			// z can be zero if there are duplicate points.
			// not sure if it should throw an exception if its "bad" or not
		}

		return( numPositive == 0 || numPositive == N );
	}

	/**
	 * Converts a rectangle into a quadrilateral
	 *
	 * @param input Rectangle.
	 * @param output Quadrilateral.  Modified.
	 */
	public static void convert( Rectangle2D_F64 input , Quadrilateral_F64 output ) {
		output.a.x = input.p0.x;
		output.a.y = input.p0.y;

		output.b.x = input.p1.x;
		output.b.y = input.p0.y;

		output.c.x = input.p1.x;
		output.c.y = input.p1.y;

		output.d.x = input.p0.x;
		output.d.y = input.p1.y;
	}

	/**
	 * Converts a rectangle into a polygon
	 *
	 * @param input Rectangle.
	 * @param output Polygon2D_F64.  Modified.
	 */
	public static void convert( Rectangle2D_F64 input , Polygon2D_F64 output ) {
		if (output.size() != 4)
			throw new IllegalArgumentException("polygon of order 4 expected");

		output.get(0).set(input.p0.x, input.p0.y);
		output.get(1).set(input.p1.x, input.p0.y);
		output.get(2).set(input.p1.x, input.p1.y);
		output.get(3).set(input.p0.x, input.p1.y);
	}

	/**
	 * Converts a quadrilateral into a polygon
	 *
	 * @param input Quadrilateral.
	 * @param output Polygon2D_F64.  Modified.
	 */
	public static void convert( Quadrilateral_F64 input , Polygon2D_F64 output ) {
		if (output.size() != 4)
			throw new IllegalArgumentException("polygon of order 4 expected");

		output.get(0).set(input.a);
		output.get(1).set(input.b);
		output.get(2).set(input.c);
		output.get(3).set(input.d);
	}

	/**
	 * Converts a polygon into a quadrilateral
	 *
	 * @param input polygon.
	 * @param output Quadrilateral.  Modified.
	 */
	public static void convert( Polygon2D_F64 input , Quadrilateral_F64 output ) {
		if( input.size() != 4 )
			throw new IllegalArgumentException("Expected 4-sided polygon as input");

		output.a.set(input.get(0));
		output.b.set(input.get(1));
		output.c.set(input.get(2));
		output.d.set(input.get(3));
	}

	/**
	 * Converts a rectangle into a quadrilateral
	 *
	 * @param input Rectangle.
	 * @param output Quadrilateral.  Modified.
	 */
	public static void convert( RectangleLength2D_I32 input , Quadrilateral_F64 output ) {
		output.a.x = input.x0;
		output.a.y = input.y0;

		output.b.x = input.x0+input.width-1;
		output.b.y = input.y0;

		output.c.x = input.x0+input.width-1;
		output.c.y = input.y0+input.height-1;

		output.d.x = input.x0;
		output.d.y = input.y0+input.height-1;
	}

	/**
	 * Finds the minimum area bounding rectangle around the quadrilateral.
	 *
	 * @param quad (Input) Quadrilateral
	 * @param rectangle (Output) Minimum area rectangle
	 */
	public static void bounding( Quadrilateral_F64 quad , Rectangle2D_F64 rectangle ) {

		rectangle.p0.x = Math.min(quad.a.x,quad.b.x);
		rectangle.p0.x = Math.min(rectangle.p0.x,quad.c.x);
		rectangle.p0.x = Math.min(rectangle.p0.x,quad.d.x);

		rectangle.p0.y = Math.min(quad.a.y,quad.b.y);
		rectangle.p0.y = Math.min(rectangle.p0.y,quad.c.y);
		rectangle.p0.y = Math.min(rectangle.p0.y,quad.d.y);

		rectangle.p1.x = Math.max(quad.a.x,quad.b.x);
		rectangle.p1.x = Math.max(rectangle.p1.x,quad.c.x);
		rectangle.p1.x = Math.max(rectangle.p1.x,quad.d.x);

		rectangle.p1.y = Math.max(quad.a.y,quad.b.y);
		rectangle.p1.y = Math.max(rectangle.p1.y,quad.c.y);
		rectangle.p1.y = Math.max(rectangle.p1.y,quad.d.y);
	}

	/**
	 * Finds the minimum area bounding rectangle around the quadrilateral that is aligned with coordinate
	 * system axises.
	 *
	 * @param polygon (Input) Polygon
	 * @param rectangle (Output) Minimum area rectangle
	 */
	public static void bounding( Polygon2D_F64 polygon , Rectangle2D_F64 rectangle ) {

		rectangle.p0.set(polygon.get(0));
		rectangle.p1.set(polygon.get(0));

		for (int i = 0; i < polygon.size(); i++) {
			Point2D_F64 p = polygon.get(i);
			if( p.x < rectangle.p0.x ) {
				rectangle.p0.x = p.x;
			} else if( p.x > rectangle.p1.x ) {
				rectangle.p1.x = p.x;
			}

			if( p.y < rectangle.p0.y ) {
				rectangle.p0.y = p.y;
			} else if( p.y > rectangle.p1.y ) {
				rectangle.p1.y = p.y;
			}
		}
	}

	/**
	 * Computes the center or average point in the quadrilateral.
	 *
	 * @param quad (Input) Quadrilateral
	 * @param center (output) Center point of the quadrilateral.  Can be null.
	 * @return The center point.
	 */
	public static Point2D_F64 center( Quadrilateral_F64 quad , Point2D_F64 center ) {
		if( center == null )
			center = new Point2D_F64();

		center.x = quad.a.x + quad.b.x + quad.c.x + quad.d.x;
		center.y = quad.a.y + quad.b.y + quad.c.y + quad.d.y;

		center.x /= 4.0;
		center.y /= 4.0;

		return center;
	}

	/**
	 * Returns true if the polygon is ordered in a counter-clockwise order.  This is done by summing up the interior
	 * angles.
	 *
	 * @param polygon List of ordered points which define a polygon
	 * @return true if CCW and false if CW
	 */
	public static boolean isCCW( List<Point2D_F64> polygon ) {
		final int N = polygon.size();
		int sign = 0;
		for (int i = 0; i < N; i++) {
			int j = (i+1)%N;
			int k = (i+2)%N;

			Point2D_F64 a = polygon.get(i);
			Point2D_F64 b = polygon.get(j);
			Point2D_F64 c = polygon.get(k);

			double dx0 = a.x-b.x;
			double dy0 = a.y-b.y;

			double dx1 = c.x-b.x;
			double dy1 = c.y-b.y;

			double z = dx0 * dy1 - dy0 * dx1;
			if( z > 0 )
				sign++;
			else
				sign--;
		}

		return sign < 0;
	}

	public static boolean isCCW( Polygon2D_F64 polygon ) {
		return isCCW(polygon.vertexes.toList());
	}

	/**
	 * Computes the average of all the vertexes
	 * @param input (input) polygon
	 * @param average (output) average point
	 */
	public static void vertexAverage(Polygon2D_F64 input, Point2D_F64 average ) {
		average.setIdx(0,0);

		for (int i = 0; i < input.size(); i++) {
			Point2D_F64 v = input.vertexes.data[i];
			average.x += v.x;
			average.y += v.y;
		}
		average.x /= input.size();
		average.y /= input.size();
	}

	/**
	 * Checks to see if the vertexes of the two polygon's are the same up to the specified tolerance
	 *
	 * @param a Polygon
	 * @param b Polygon
	 * @param tol tolerance
	 * @return true if identical up to tolerance or false if not
	 */
	public static boolean isIdentical( Polygon2D_F64 a , Polygon2D_F64 b , double tol ) {
		if( a.size() != b.size())
			return false;

		double tol2 = tol*tol;
		for (int i = 0; i < a.size(); i++) {
			if( a.get(i).distance2(b.get(i)) > tol2 )
				return false;
		}
		return true;
	}

	/**
	 * Checks to see if the vertexes of the two polygon's are the same up to the specified tolerance and allows
	 * for a shift in their order
	 *
	 * @param a Polygon
	 * @param b Polygon
	 * @param tol tolerance
	 * @return true if identical up to tolerance or false if not
	 */
	public static boolean isEquivalent( Polygon2D_F64 a , Polygon2D_F64 b , double tol ) {
		if( a.size() != b.size())
			return false;

		double tol2 = tol*tol;

		// first find two vertexes which are the same
		Point2D_F64 a0 = a.get(0);
		int match = -1;
		for (int i = 0; i < b.size(); i++) {
			if( a0.distance2(b.get(i)) <= tol2 ) {
				match = i;
				break;
			}
		}

		if( match < 0 )
			return false;

		// now go in a circle and see if they all line up
		for (int i = 1; i < b.size(); i++) {
			Point2D_F64 ai = a.get(i);
			Point2D_F64 bi = b.get((match+i)%b.size());

			if( ai.distance2(bi) > tol2 ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Flips the order of points inside the polygon.  The first index will remain the same will otherwise be reversed
	 *
	 * @param a Polygon of order 3 or more.
	 */
	public static void flip( Polygon2D_F64 a ) {
		int N = a.size();
		int H = N/2;

		for (int i = 1; i <= H; i++) {
			int j = N-i;
			Point2D_F64 tmp = a.vertexes.data[i];
			a.vertexes.data[i] = a.vertexes.data[j];
			a.vertexes.data[j] = tmp;
		}
	}

	/**
	 * Shifts all the vertexes in the polygon up one element.  Wraps around at the end
	 * @param a Polygon
	 */
	public static void shiftUp( Polygon2D_F64 a ) {
		final int N = a.size();

		Point2D_F64 first = a.get(0);

		for (int i = 0; i < N-1; i++ ) {
			a.vertexes.data[i] = a.vertexes.data[i+1];
		}
		a.vertexes.data[N-1] = first;
	}

	/**
	 * Shifts all the vertexes in the polygon up one element.  Wraps around at the end
	 * @param a Polygon
	 */
	public static void shiftDown( Polygon2D_F64 a ) {
		final int N = a.size();

		Point2D_F64 last = a.get(N-1);

		for (int i = N-1; i > 0; i-- ) {
			a.vertexes.data[i] = a.vertexes.data[i-1];
		}
		a.vertexes.data[0] = last;
	}

	/**
	 * Computes the convex hull of the set of points.
	 *
	 * <p>
	 * NOTE: This method declares a temporary array.  If you want to avoid that invoke {@link AndrewMonotoneConvexHull_F64}
	 * directly.
	 * </p>
	 *
	 * @param points (Input) Set of points.
	 * @param hull (output) storage for convex hull.  Will be in counter-clockwise order
	 */
	public static void convexHull( List<Point2D_F64> points , Polygon2D_F64 hull ) {
		Point2D_F64[] array = new Point2D_F64[points.size()];

		for (int i = 0; i < points.size(); i++) {
			array[i] = points.get(i);
		}

		AndrewMonotoneConvexHull_F64 andrew = new AndrewMonotoneConvexHull_F64();
		andrew.process(array,array.length,hull);
	}

	/**
	 * Removes a node from a polygon if the two lines its attached two are almost parallel
	 * @param polygon The polygon being modified
	 * @param tol Tolerance in radians
	 */
	public static void removeAlmostParallel( Polygon2D_F64 polygon , double tol ) {

		for (int i = 0; i < polygon.vertexes.size(); ) {
			int j = (i+1)%polygon.vertexes.size();
			int k = (i+2)%polygon.vertexes.size();

			Point2D_F64 p0 = polygon.vertexes.get(i);
			Point2D_F64 p1 = polygon.vertexes.get(j);
			Point2D_F64 p2 = polygon.vertexes.get(k);

			double angle = UtilVector2D_F64.acute(p1.x-p0.x,p1.y-p0.y,p2.x-p1.x,p2.y-p1.y);

			if( angle <= tol) {
				polygon.vertexes.remove(j);
				if( j < i )
					i = polygon.vertexes.size()-1;
			} else {
				i++;
			}
		}
	}

	/**
	 * Remove a point if it's identical to a neighbor
	 *
	 * @param polygon The polygon being modified
	 * @param tol Tolerance in radians
	 */
	public static void removeAdjacentDuplicates( Polygon2D_F64 polygon , double tol ) {

		for (int i = polygon.vertexes.size()-1,j=0; i >= 0 && polygon.size()>1; j=i,i--) {
			if( polygon.get(i).isIdentical(polygon.get(j),tol)) {
				polygon.vertexes.remove(i);
			}
		}
	}

	/**
	 * Remove a point if it's identical to a neighbor
	 *
	 * @param polygon The polygon being modified
	 * @param tol Tolerance in radians
	 */
	public static boolean hasAdjacentDuplicates( Polygon2D_F64 polygon , double tol ) {

		for (int i = polygon.vertexes.size()-1,j=0; i >= 0 && polygon.size()>1; j=i,i--) {
			if( polygon.get(i).isIdentical(polygon.get(j),tol)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Compute the error as a function of the distance between the model and target. The target is sampled at regular
	 * intervals and for each of these points the closest point on the model is found. The returned metric is the
	 * average of difference between paired points.
	 *
	 * NOTE: A different answer will be returned depending on which polygon is the model and which one is the target.
	 *
	 * @param model Model polygon
	 * @param target Target polygon
	 * @return average of closest point error
	 */
	public static double averageOfClosestPointError(Polygon2D_F64 model , Polygon2D_F64 target , int numberOfSamples ) {
		LineSegment2D_F64 line = new LineSegment2D_F64();

		double cornerLocationsB[] = new double[target.size()+1];
		double totalLength = 0;
		for (int i = 0; i < target.size(); i++) {
			Point2D_F64 b0 = target.get(i%target.size());
			Point2D_F64 b1 = target.get((i+1)%target.size());

			cornerLocationsB[i] = totalLength;
			totalLength += b0.distance(b1);
		}
		cornerLocationsB[target.size()] = totalLength;

		Point2D_F64 pointOnB = new Point2D_F64();
		double error = 0;
		int cornerB = 0;
		for (int k = 0; k < numberOfSamples; k++) {
			// Find the point on B to match to a point on A
			double location = totalLength*k/numberOfSamples;

			while (location > cornerLocationsB[cornerB + 1]) {
				cornerB++;
			}
			Point2D_F64 b0 = target.get(cornerB);
			Point2D_F64 b1 = target.get((cornerB+1)%target.size());

			double locationCornerB = cornerLocationsB[cornerB];
			double fraction = (location-locationCornerB)/(cornerLocationsB[cornerB+1]-locationCornerB);

			pointOnB.x = (b1.x-b0.x)*fraction + b0.x;
			pointOnB.y = (b1.y-b0.y)*fraction + b0.y;

			// find the best fit point on A to the point in B
			double best = Double.MAX_VALUE;
			for (int i = 0; i < model.size()+1; i++) {
				line.a = model.get(i%model.size());
				line.b = model.get((i+1)%model.size());

				double d = Distance2D_F64.distance(line,pointOnB);
				if( d < best ) {
					best = d;
				}
			}
			error += best;
		}

		return error/numberOfSamples;
	}

}


/*
 * Copyright (C) 2011-2017, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.struct.homography;

import org.ejml.data.DMatrixRMaj;
import org.ejml.dense.row.CommonOps_DDRM;

/**
 * Various useful functions related to homographies.
 *
 * @author Peter Abeles
 */
public class UtilHomography_F64 {

	/**
	 * Converts the provided 3x3 matrix into a {@link Homography2D_F64}.
	 *
	 * @param m Input 3x3 matrix.
	 * @param ret Storage for output.  If null then a new instance is created.
	 * @return Equivalent homography.
	 */
	public static Homography2D_F64 convert( DMatrixRMaj m , Homography2D_F64 ret ) {
		if( m.numCols != 3 || m.numRows != 3)
			throw new IllegalArgumentException("Expected a 3 by 3 matrix.");

		if( ret == null )
			ret = new Homography2D_F64();

		ret.a11 = m.unsafe_get(0,0);
		ret.a12 = m.unsafe_get(0,1);
		ret.a13 = m.unsafe_get(0,2);
		ret.a21 = m.unsafe_get(1,0);
		ret.a22 = m.unsafe_get(1,1);
		ret.a23 = m.unsafe_get(1,2);
		ret.a31 = m.unsafe_get(2,0);
		ret.a32 = m.unsafe_get(2,1);
		ret.a33 = m.unsafe_get(2,2);

		return ret;
	}

	/**
	 * Converts a {@link Homography2D_F64} into a 3x3 matrix.
	 *
	 * @param m Homography
	 * @param ret Storage for output.  If null then a new instance is created.
	 * @return Equivalent matrix.
	 */
	public static DMatrixRMaj convert( Homography2D_F64 m , DMatrixRMaj ret ) {
		if( ret == null ) {
			ret = new DMatrixRMaj(3,3);
		} else if( ret.numCols != 3 || ret.numRows != 3)
			throw new IllegalArgumentException("Expected a 3 by 3 matrix.");

		ret.unsafe_set(0,0,m.a11);
		ret.unsafe_set(0,1,m.a12);
		ret.unsafe_set(0,2,m.a13);
		ret.unsafe_set(1,0,m.a21);
		ret.unsafe_set(1,1,m.a22);
		ret.unsafe_set(1,2,m.a23);
		ret.unsafe_set(2,0,m.a31);
		ret.unsafe_set(2,1,m.a32);
		ret.unsafe_set(2,2,m.a33);

		return ret;
	}

	public static Homography2D_F64 invert( Homography2D_F64 orig , Homography2D_F64 inverted ) {
		if( inverted == null )
			inverted = new Homography2D_F64();

		DMatrixRMaj A = new DMatrixRMaj(3,3);
		convert(orig,A);
		CommonOps_DDRM.invert(A);
		convert(A,inverted);

		return inverted;
	}

	public static void scale( Homography2D_F64 h , double scale ) {
		h.a11 *= scale;
		h.a12 *= scale;
		h.a13 *= scale;
		h.a21 *= scale;
		h.a22 *= scale;
		h.a23 *= scale;
		h.a31 *= scale;
		h.a32 *= scale;
		h.a33 *= scale;
	}

   public static void print(Homography2D_F64 h)
   {
      for( int y = 0; y < 3; y++ ) {
         for( int x = 0; x < 3; x++ ) {
            System.out.printf("%8.1e ",h.get(x,y));
         }
         System.out.println();
      }
   }
}


/*
 * Copyright (C) 2011-2017, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.geometry.algs;

import georegression.geometry.UtilPolygons2D_F64;
import georegression.struct.point.Point2D_I32;
import georegression.struct.shapes.Polygon2D_F64;
import georegression.struct.shapes.Rectangle2D_F64;

/**
 * <p>
 * Computes the area of intersection between two convex polygons. Port of code found at [1] and Java version by Lagado.
 * </p>
 *
 * WARNING: No effort has been made to reduce the number of calls to new internally from original code.
 *
 * [1] http://www.cap-lore.com/MathPhys/IP/
 *
 * @author Peter Abeles
 */
public class AreaIntersectionPolygon2D_F64 {
	static final double gamut = (double)500000000.0;
	static final double mid = gamut / (double)2.0;

	private long ssss;
	private double sclx;
	private double scly;

	/**
	 * Computes the area of the intersection between the two polygons.
	 *
	 * Note: the area result has little more accuracy than a float
	 *  This is true even if the polygon is specified with doubles.
	 *
	 * @param a Polygon A
	 * @param b Polygon B
	 * @return area of area of intersection. Negative if the order (CW vs CCW) do not match.
	 */
	public double computeArea(Polygon2D_F64 a , Polygon2D_F64 b ) {
		ssss = 0;
		sclx = 0;
		scly = 0;
		return inter(a,b);
	}

	//--------------------------------------------------------------------------

	static class Rng {
		int mn; int mx;
		Rng(int mn, int mx) { this.mn = mn; this.mx = mx; }
	}
	static class Vertex { Point2D_I32 ip; Rng rx; Rng ry; int in; }

	//--------------------------------------------------------------------------

	private static void range(Polygon2D_F64 points, Rectangle2D_F64 bbox)
	{
		UtilPolygons2D_F64.bounding(points,bbox);
	}

	private static long area(Point2D_I32 a, Point2D_I32 p, Point2D_I32 q) {
		return (long)p.x * q.y - (long)p.y * q.x +
				(long)a.x * (p.y - q.y) + (long)a.y * (q.x - p.x);
	}

	private static boolean ovl(Rng p, Rng q) {
		return p.mn < q.mx && q.mn < p.mx;
	}

	private void cntrib(int f_x, int f_y, int t_x, int t_y, int w) {
		ssss += (long)w * (t_x - f_x) * (t_y + f_y) / 2;
	}

	private void fit(Polygon2D_F64 x, Vertex[] ix, int fudge, Rectangle2D_F64 B)
	{
		int c = x.size();
		while (c-- > 0) {
			ix[c] = new Vertex();
			ix[c].ip = new Point2D_I32();
			ix[c].ip.x = ((int)((x.get(c).getX() - B.p0.x) * sclx - mid) & ~7)
					| fudge | (c & 1);
			ix[c].ip.y = ((int)((x.get(c).getY() - B.p0.y) * scly - mid) & ~7)
					| fudge;
		}

		ix[0].ip.y += x.size() & 1;
		ix[x.size()] = ix[0];

		c = x.size();
		while (c-- > 0) {
			ix[c].rx = ix[c].ip.x < ix[c + 1].ip.x ?
					new Rng(ix[c].ip.x, ix[c + 1].ip.x) :
					new Rng(ix[c + 1].ip.x, ix[c].ip.x);
			ix[c].ry = ix[c].ip.y < ix[c + 1].ip.y ?
					new Rng(ix[c].ip.y, ix[c + 1].ip.y) :
					new Rng(ix[c + 1].ip.y, ix[c].ip.y);
			ix[c].in = 0;
		}
	}

	private void cross(Vertex a, Vertex b, Vertex c, Vertex d,
		  double a1, double a2, double a3, double a4)
	{
		double r1 = a1 / ((double) a1 + a2);
		double r2 = a3 / ((double) a3 + a4);

		cntrib((int)(a.ip.x + r1 * (b.ip.x - a.ip.x)),
				(int)(a.ip.y + r1 * (b.ip.y - a.ip.y)),
				b.ip.x, b.ip.y, 1);
		cntrib(d.ip.x, d.ip.y,
				(int)(c.ip.x + r2 * (d.ip.x - c.ip.x)),
				(int)(c.ip.y + r2 * (d.ip.y - c.ip.y)),
				1);
		++a.in;
		--c.in;
	}

	private void inness(Vertex[] P, int cP, Vertex[] Q, int cQ)
	{
		int s = 0;
		int c = cQ;
		Point2D_I32 p = P[0].ip;

		while (c-- > 0) {
			if (Q[c].rx.mn < p.x && p.x < Q[c].rx.mx) {
				boolean sgn = 0 < area(p, Q[c].ip, Q[c + 1].ip);
				s += (sgn != Q[c].ip.x < Q[c + 1].ip.x) ? 0 : (sgn ? -1 : 1);
			}
		}
		for (int j = 0; j < cP; ++j) {
			if (s != 0)
				cntrib(P[j].ip.x, P[j].ip.y,
						P[j + 1].ip.x, P[j + 1].ip.y, s);
			s += P[j].in;
		}
	}

	//-------------------------------------------------------------------------

	private double inter(Polygon2D_F64 a, Polygon2D_F64 b)
	{
		if (a.size() < 3 || b.size() < 3)
			return 0;

		//		int na = a.size();
//		int nb = b.size();
		Vertex[] ipa = new Vertex[a.size() + 1];
		Vertex[] ipb = new Vertex[b.size()+ 1];
		Rectangle2D_F64 bbox = new Rectangle2D_F64(
				Double.MAX_VALUE, Double.MAX_VALUE,
				-Double.MAX_VALUE, -Double.MAX_VALUE);

		range(a, bbox);
		range(b, bbox);

		double rngx = bbox.p1.x - bbox.p0.x;
		sclx = gamut / rngx;
		double rngy = bbox.p1.y - bbox.p0.y;
		scly = gamut / rngy;
		double ascale = sclx * scly;

		fit(a, ipa, 0, bbox);
		fit(b, ipb, 2, bbox);

		for (int j = 0; j < a.size(); ++j) {
			for (int k = 0; k < b.size(); ++k) {
				if (ovl(ipa[j].rx, ipb[k].rx) && ovl(ipa[j].ry, ipb[k].ry)) {
					long a1 = -area(ipa[j].ip, ipb[k].ip, ipb[k + 1].ip);
					long a2 = area(ipa[j + 1].ip, ipb[k].ip, ipb[k + 1].ip);
					boolean o = a1 < 0;
					if (o == a2 < 0) {
						long a3 = area(ipb[k].ip, ipa[j].ip, ipa[j + 1].ip);
						long a4 = -area(ipb[k + 1].ip, ipa[j].ip,
								ipa[j + 1].ip);
						if (a3 < 0 == a4 < 0) {
							if (o)
								cross(ipa[j], ipa[j + 1], ipb[k], ipb[k + 1],
										a1, a2, a3, a4);
							else
								cross(ipb[k], ipb[k + 1], ipa[j], ipa[j + 1],
										a3, a4, a1, a2);
						}
					}
				}
			}
		}

		inness(ipa, a.size(), ipb, b.size());
		inness(ipb, b.size(), ipa, a.size());

		return ssss / ascale;
	}
}


/*
 * Copyright (C) 2011-2019, Peter Abeles. All Rights Reserved.
 *
 * This file is part of Geometric Regression Library (GeoRegression).
 *
 * Licensed 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 georegression.struct.line;

import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point2D_I32;

import java.io.Serializable;

/**
 *

 * 2D line parameterized using parametric equation:

 * [x, y] = [x_0, y_0] + tÂ·[slopeX, slopeY]

 * where t specifies the location along the line, (x_0,y_0) is an arbitrary point on the line,
 * and (slopeX,slopeY).
 *

 *
 * @see georegression.geometry.UtilLine2D_I32
 */
public class LineParametric2D_I32 implements Serializable {
	/**
	 * A point on the line
	 */
	public Point2D_I32 p = new Point2D_I32();
	/**
	 * The line's slope
	 */
	public int slopeX,slopeY;

	public LineParametric2D_I32(int x_0, int y_0, int slopeX, int slopeY ) {
		p.set( x_0, y_0 );
		this.slopeX = slopeX;
		this.slopeY = slopeY;
	}

	public LineParametric2D_I32(Point2D_I32 p, int slopeX, int slopeY ) {
		setPoint(p);
		setSlope(slopeX,slopeY);
	}

	public LineParametric2D_I32() {
	}

	public void set( LineParametric2D_I32 line ) {
		this.p.set(line.p);
		this.slopeX = line.slopeX;
		this.slopeY = line.slopeY;
	}

	public void setPoint( Point2D_I32 pt ) {
		this.p.set( pt );
	}

	public void setPoint( int x, int y ) {
		this.p.x = x;
		this.p.y = y;
	}

	public void setSlope( int slopeX , int slopeY ) {
		this.slopeX = slopeX;
		this.slopeY = slopeY;
	}

	/**
	 * Returns a point along the line.  See parametric equation in class description.
	 *
	 * @param t Location along the line.
	 * @return Point on the line.
	 */
	public Point2D_F64 getPointOnLine( double t ) {
		return new Point2D_F64( slopeX * t + p.x, slopeY * t + p.y );
	}

	public Point2D_I32 getPoint() {
		return p;
	}

	public final int getSlopeX() {
		return slopeX;
	}

	public final int getSlopeY() {
		return slopeY;
	}

	public final int getX() {
		return p.x;
	}

	public final int getY() {
		return p.y;
	}

	public Point2D_I32 getP() {
		return p;
	}

	public void setP(Point2D_I32 p) {
		this.p = p;
	}

	public LineParametric2D_I32 copy() {
		return new LineParametric2D_I32( p, slopeX, slopeY );
	}

	@Override
	public String toString() {
		return getClass().getSimpleName()+" P( "+p.x+" "+p.y+" ) Slope( "+slopeX+" "+slopeY+" )";
	}
}