package org.infobip.mobile.messaging.geo.report;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.ArraySet;

import org.infobip.mobile.messaging.Message;
import org.infobip.mobile.messaging.MobileMessagingCore;
import org.infobip.mobile.messaging.logging.MobileMessagingLogger;
import org.infobip.mobile.messaging.geo.Area;
import org.infobip.mobile.messaging.geo.Geo;
import org.infobip.mobile.messaging.geo.GeoEventType;
import org.infobip.mobile.messaging.geo.GeoLatLng;
import org.infobip.mobile.messaging.geo.geofencing.GeofencingHelper;
import org.infobip.mobile.messaging.geo.mapper.GeoDataMapper;
import org.infobip.mobile.messaging.geo.transition.GeoNotificationHelper;
import org.infobip.mobile.messaging.platform.Time;
import org.infobip.mobile.messaging.storage.MessageStore;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * @author sslavin
 * @since 07/02/2017.
 */

public class GeoReportHelper {

    /**
     * Returns signaling message for geofencing report
     *
     * @param messages all available messages
     * @param report   geofencing event report
     * @return corresponding signaling message
     */
    @Nullable
    public static Message getSignalingMessageForReport(List<Message> messages, GeoReport report) {
        for (Message m : messages) {
            if (!m.getMessageId().equals(report.getSignalingMessageId())) {
                continue;
            }
            return m;
        }
        return null;
    }

    /**
     * Creates new geo notification messages based on reporting result
     *
     * @param reportedEvents  events that were reported to server
     * @param reportingResult response from the server
     * @return map of messages and corresponding geo event types for each message
     */
    public static Map<Message, GeoEventType> createMessagesToNotify(Context context, List<GeoReport> reportedEvents, @NonNull GeoReportingResult reportingResult) {
        GeofencingHelper geofencingHelper = new GeofencingHelper(context);
        List<Message> allMessages = geofencingHelper.getMessageStoreForGeo().findAll(context);
        Map<Message, GeoEventType> messages = new ArrayMap<>();
        for (GeoReport report : reportedEvents) {
            Message signalingMessage = GeoReportHelper.getSignalingMessageForReport(allMessages, report);
            if (signalingMessage == null) {
                MobileMessagingLogger.e("Cannot find signaling message for id: " + report.getSignalingMessageId());
                continue;
            }

            messages.put(createNewMessageForReport(report, reportingResult, signalingMessage), report.getEvent());
        }
        return messages;
    }

    /**
     * Generates set of geofencing reports for multiple signaling messages (areas from multiple messages can be triggered at the same time).
     *
     * @param messagesAndAreas   map of signaling message and triggered geofence areas
     * @param event              transition type
     * @param triggeringLocation event location
     * @return list of geofencing reports for the provided messages and areas
     */
    public static GeoReport[] createReportsForMultipleMessages(Context context, Map<Message, List<Area>> messagesAndAreas, @NonNull GeoEventType event, @NonNull GeoLatLng triggeringLocation) {
        List<GeoReport> reports = new ArrayList<>();
        for (Message message : messagesAndAreas.keySet()) {
            List<Area> areas = messagesAndAreas.get(message);
            if (areas != null) {
                reports.addAll(createReports(context, message, areas, event, triggeringLocation));
            }
        }
        return reports.toArray(new GeoReport[0]);
    }

    /**
     * Generates set of geofencing reports
     *
     * @param context          context
     * @param signalingMessage original signaling message
     * @param areas            list of areas that triggered this geofencing event
     * @param event            transition type
     * @return set of geofencing reports to send to server
     */
    private static Set<GeoReport> createReports(Context context, Message signalingMessage, List<Area> areas, @NonNull GeoEventType event, @NonNull GeoLatLng triggeringLocation) {
        Set<GeoReport> reports = new ArraySet<>();
        for (Area area : areas) {
            GeoReport report = createReport(signalingMessage, area, event, triggeringLocation);
            reports.add(report);
            MobileMessagingCore.getInstance(context).addGeneratedMessageIds(report.getMessageId());
        }
        return reports;
    }

    /**
     * Generates geofencing event report
     *
     * @param signalingMessage original signaling push message with geofences
     * @param area             area that triggered the event
     * @param event            transition type
     * @return generated report with unique messageId or null if no report available for this geofence and transition
     */
    @NonNull
    private static GeoReport createReport(Message signalingMessage, Area area, @NonNull GeoEventType event, @NonNull GeoLatLng triggeringLocation) {
        Geo geo = GeoDataMapper.geoFromInternalData(signalingMessage.getInternalData());
        return new GeoReport(
                geo == null ? "" : geo.getCampaignId(),
                UUID.randomUUID().toString(),
                signalingMessage.getMessageId(),
                event,
                area,
                Time.now(),
                triggeringLocation
        );
    }

    /**
     * Creates new message based on geofencing report
     *
     * @param report          geofencing report for any supported event
     * @param reportingResult result of reporting geo events to server
     * @param originalMessage original signaling message
     * @return new message based on triggering event, area and original signaling message
     */
    private static Message createNewMessageForReport(@NonNull final GeoReport report, @NonNull GeoReportingResult reportingResult, @NonNull Message originalMessage) {
        GeoLatLng triggeringLocation = report.getTriggeringLocation();
        if (triggeringLocation == null) {
            triggeringLocation = new GeoLatLng(null, null);
        }

        List<Area> areas = new ArrayList<>();
        if (report.getArea() != null) {
            areas.add(report.getArea());
        }

        Geo geo;
        Geo originalMessageGeo = GeoDataMapper.geoFromInternalData(originalMessage.getInternalData());

        if (originalMessageGeo != null) {
            geo = new Geo(triggeringLocation.getLat(),
                    triggeringLocation.getLng(),
                    originalMessageGeo.getDeliveryTime(),
                    originalMessageGeo.getExpiryTime(),
                    originalMessageGeo.getStartTime(),
                    originalMessageGeo.getCampaignId(),
                    areas,
                    originalMessageGeo.getEvents(),
                    originalMessage.getSentTimestamp(),
                    originalMessage.getContentUrl());
        } else {
            geo = new Geo(triggeringLocation.getLat(),
                    triggeringLocation.getLng(),
                    null, null, null, null,
                    areas,
                    null,
                    Time.now(),
                    originalMessage.getContentUrl());
        }

        String internalData = GeoDataMapper.geoToInternalData(geo);
        return new Message(
                getMessageIdFromReport(report, reportingResult),
                originalMessage.getTitle(),
                originalMessage.getBody(),
                originalMessage.getSound(),
                originalMessage.isVibrate(),
                originalMessage.getIcon(),
                false, // enforcing non-silent
                originalMessage.getCategory(),
                originalMessage.getFrom(),
                Time.now(),
                0,
                Time.now(),
                originalMessage.getCustomPayload(),
                internalData,
                originalMessage.getDestination(),
                originalMessage.getStatus(),
                originalMessage.getStatusMessage(),
                originalMessage.getContentUrl(),
                originalMessage.getInAppStyle(),
                originalMessage.getInAppExpiryTimestamp(),
                originalMessage.getWebViewUrl(),
                originalMessage.getMessageType()
        );
    }

    /**
     * Retrieves message id based on reporting result (or based or report itself if result is not available now)
     *
     * @param report          geo event report to the server
     * @param reportingResult result of reporting
     * @return appropriate message id
     */
    private static String getMessageIdFromReport(@NonNull GeoReport report, @NonNull GeoReportingResult reportingResult) {
        if (reportingResult.getMessageIds() == null || reportingResult.getMessageIds().isEmpty()) {
            return report.getMessageId();
        }

        String newMessageId = reportingResult.getMessageIds().get(report.getMessageId());
        if (newMessageId == null) {
            return report.getMessageId();
        }

        return newMessageId;
    }

    /**
     * Returns list of inactive campaign ids based on reporting result
     *
     * @param result response from the server
     * @return set of inactive campaign ids
     */
    public static Set<String> getAndUpdateInactiveCampaigns(Context context, GeoReportingResult result) {
        Set<String> inactiveCampaigns = new ArraySet<>();
        if (result == null || result.hasError()) {
            inactiveCampaigns.addAll(GeofencingHelper.getSuspendedCampaignIds(context));
            inactiveCampaigns.addAll(GeofencingHelper.getFinishedCampaignIds(context));
            return inactiveCampaigns;
        }

        GeofencingHelper.addCampaignStatus(context, result.getFinishedCampaignIds(), result.getSuspendedCampaignIds());
        if (result.getFinishedCampaignIds() != null) {
            inactiveCampaigns.addAll(result.getFinishedCampaignIds());
        }
        if (result.getSuspendedCampaignIds() != null) {
            inactiveCampaigns.addAll(result.getSuspendedCampaignIds());
        }
        return inactiveCampaigns;
    }

    /**
     * Returns map of signaling messages and corresponding areas that match geofence and transition event
     *
     * @param messageStore message store to look messages for
     * @param requestIds   requestIds received from Google Location Services during transition event
     * @param event        transition event type
     * @return signaling messages with corresponding areas
     */
    @NonNull
    public static Map<Message, List<Area>> findSignalingMessagesAndAreas(Context context, MessageStore messageStore, Set<String> requestIds, @NonNull GeoEventType event) {
        Date now = Time.date();
        Map<Message, List<Area>> messagesAndAreas = new ArrayMap<>();
        for (Message message : messageStore.findAll(context)) {
            Geo geo = GeoDataMapper.geoFromInternalData(message.getInternalData());
            if (geo == null || geo.getAreasList() == null || geo.getAreasList().isEmpty()) {
                continue;
            }

            //don't trigger geo event before start date
            Date startDate = geo.getStartDate();
            if (startDate != null && startDate.after(now)) {
                continue;
            }

            List<Area> campaignAreas = geo.getAreasList();
            List<Area> triggeredAreas = new ArrayList<>();
            for (Area area : campaignAreas) {
                for (String requestId : requestIds) {
                    if (!requestId.equalsIgnoreCase(area.getId())) {
                        continue;
                    }

                    if (!GeoNotificationHelper.shouldReportTransition(context, geo, event)) {
                        continue;
                    }

                    triggeredAreas.add(area);
                }
            }

            if (!triggeredAreas.isEmpty()) {
                messagesAndAreas.put(message, triggeredAreas);
            }
        }

        return filterOverlappingAreas(messagesAndAreas);
    }

    /**
     * Filters out geo reports based on campaign status
     *
     * @param reports all reports sent to the server
     * @param result  result of reporting geofencing event reports to server
     * @return list of reports for active campaigns
     */
    public static List<GeoReport> filterOutNonActiveReports(Context context, @NonNull List<GeoReport> reports, @NonNull GeoReportingResult result) {
        if (result.hasError()) {
            return reports;
        }

        Set<String> inactiveCampaigns = getAndUpdateInactiveCampaigns(context, result);
        if (inactiveCampaigns.isEmpty()) {
            return reports;
        }

        List<GeoReport> activeReports = new ArrayList<>();
        for (GeoReport r : reports) {
            if (inactiveCampaigns.contains(r.getCampaignId())) {
                continue;
            }

            activeReports.add(r);
        }
        return activeReports;
    }

    /**
     * Filters out overlapping areas for each campaign and returns only the smallest area
     *
     * @param messagesAndAreas all triggered areas for each message
     * @return filtered areas
     */
    public static Map<Message, List<Area>> filterOverlappingAreas(Map<Message, List<Area>> messagesAndAreas) {
        Map<Message, List<Area>> filteredMessagesAndAreas = new ArrayMap<>(messagesAndAreas.size());

        for (Map.Entry<Message, List<Area>> entry : messagesAndAreas.entrySet()) {
            Message message = entry.getKey();
            List<Area> areasList = entry.getValue();

            if (areasList != null) {
                //using only area that has the smallest radius
                Collections.sort(areasList, new GeoAreaRadiusComparator());
                filteredMessagesAndAreas.put(message, Collections.singletonList(areasList.get(0)));
            }
        }

        return filteredMessagesAndAreas;
    }

    /**
     * Compares areas by radius
     */
    public static class GeoAreaRadiusComparator implements Comparator<Area> {

        @Override
        public int compare(Area area1, Area area2) {
            return area1.getRadius() - area2.getRadius();
        }
    }
}