/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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
 *
 *   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.apache.streams.twitter.converter.util;

import org.apache.streams.exceptions.ActivityConversionException;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.pojo.extensions.ExtensionUtil;
import org.apache.streams.pojo.json.Activity;
import org.apache.streams.pojo.json.ActivityObject;
import org.apache.streams.pojo.json.Image;
import org.apache.streams.pojo.json.Provider;
import org.apache.streams.twitter.Url;
import org.apache.streams.twitter.pojo.Delete;
import org.apache.streams.twitter.pojo.Entities;
import org.apache.streams.twitter.pojo.Hashtag;
import org.apache.streams.twitter.pojo.Place;
import org.apache.streams.twitter.pojo.Retweet;
import org.apache.streams.twitter.pojo.TargetObject;
import org.apache.streams.twitter.pojo.Tweet;
import org.apache.streams.twitter.pojo.User;
import org.apache.streams.twitter.pojo.UserMentions;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.math.DoubleMath.mean;

/**
 * Provides utilities for working with Activity objects within the context of Twitter.
 */
public class TwitterActivityUtil {

  private static final Logger LOGGER = LoggerFactory.getLogger(TwitterActivityUtil.class);

  private static final ObjectMapper mapper = StreamsJacksonMapper.getInstance();

  /**
   * Updates the given Activity object with the values from the Tweet.
   * @param tweet the object to use as the source
   * @param activity the target of the updates.  Will receive all values from the tweet.
   * @throws ActivityConversionException ActivityConversionException
   */
  public static void updateActivity(Tweet tweet, Activity activity) throws ActivityConversionException {
    activity.setActor(buildActor(tweet));
    activity.setId(formatId(activity.getVerb(),
        Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
            .orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null)));

    if (tweet instanceof Retweet) {
      updateActivityContent(activity,  (tweet).getRetweetedStatus(), "share");
    } else {
      updateActivityContent(activity, tweet, "post");
    }

    if (StringUtils.isBlank(activity.getId())) {
      throw new ActivityConversionException("Unable to determine activity id");
    }
    try {
      activity.setPublished(new DateTime(tweet.getCreatedAt()));
    } catch ( Exception ex ) {
      throw new ActivityConversionException("Unable to determine publishedDate", ex);
    }
    activity.setTarget(buildTarget(tweet));
    activity.setProvider(getProvider());
    activity.setUrl(String.format("http://twitter.com/%s/%s/%s", tweet.getUser().getScreenName(),"/status/",tweet.getIdStr()));

    addTwitterExtension(activity, mapper.convertValue(tweet, ObjectNode.class));
  }

  /**
   * Updates the given Activity object with the values from the User
   * @param user the object to use as the source
   * @param activity the target of the updates.  Will receive all values from the tweet.
   */
  public static void updateActivity(User user, Activity activity) {
    activity.setActor(buildActor(user));
    activity.setId(null);
    activity.setVerb(null);
  }

  /**
   * Updates the activity for a delete event.
   * @param delete the delete event
   * @param activity the Activity object to update
   * @throws ActivityConversionException ActivityConversionException
   */
  public static void updateActivity(Delete delete, Activity activity) throws ActivityConversionException {
    activity.setActor(buildActor(delete));
    activity.setVerb("delete");
    activity.setObject(buildActivityObject(delete));
    activity.setId(formatId(activity.getVerb(), delete.getDelete().getStatus().getIdStr()));
    if (StringUtils.isBlank(activity.getId())) {
      throw new ActivityConversionException("Unable to determine activity id");
    }
    activity.setProvider(getProvider());
    addTwitterExtension(activity, StreamsJacksonMapper.getInstance().convertValue(delete, ObjectNode.class));
  }

  /**
   * Builds the activity {@link ActivityObject} actor from the tweet.
   * @param tweet the object to use as the source
   * @return a valid Actor populated from the Tweet
   */
  public static ActivityObject buildActor(Tweet tweet) {
    ActivityObject actor = new ActivityObject();
    User user = tweet.getUser();

    return buildActor(user);
  }

  /**
   * Builds the activity {@link ActivityObject} actor from the User.
   * @param user the object to use as the source
   * @return a valid Actor populated from the Tweet
   */
  public static ActivityObject buildActor(User user) {
    ActivityObject actor = new ActivityObject();
    actor.setId(formatId(
        Optional.ofNullable(Optional.ofNullable(user.getIdStr())
            .orElseGet(Optional.of(user.getId().toString())::get)).orElse(null)
    ));
    actor.setObjectType("page");
    actor.setDisplayName(user.getName());
    actor.setAdditionalProperty("handle", user.getScreenName());
    actor.setSummary(user.getDescription());

    if (user.getUrl() != null) {
      actor.setUrl(user.getUrl());
    }

    Map<String, Object> extensions = new HashMap<>();
    extensions.put("location", user.getLocation());
    extensions.put("posts", user.getStatusesCount());
    extensions.put("favorites", user.getFavouritesCount());
    extensions.put("followers", user.getFollowersCount());

    Image profileImage = new Image();
    profileImage.setUrl(user.getProfileImageUrlHttps());
    actor.setImage(profileImage);

    extensions.put("screenName", user.getScreenName());

    actor.setAdditionalProperty("extensions", extensions);
    return actor;
  }

  /**
   * Builds the actor for a delete event.
   * @param delete the delete event
   * @return a valid Actor
   */
  public static ActivityObject buildActor(Delete delete) {
    ActivityObject actor = new ActivityObject();
    actor.setId(formatId(delete.getDelete().getStatus().getUserIdStr()));
    actor.setObjectType("page");
    return actor;
  }

  /**
   * Creates an {@link ActivityObject} for the tweet.
   * @param tweet the object to use as the source
   * @return a valid ActivityObject
   */
  public static ActivityObject buildActivityObject(Tweet tweet) {
    ActivityObject actObj = new ActivityObject();
    String id = Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
        .orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null);
    if ( id != null ) {
      actObj.setId(id);
    }
    actObj.setObjectType("post");
    actObj.setContent(tweet.getText());
    return actObj;
  }

  /**
   * Builds the ActivityObject for the delete event.
   * @param delete the delete event
   * @return a valid Activity Object
   */
  public static ActivityObject buildActivityObject(Delete delete) {
    ActivityObject actObj = new ActivityObject();
    actObj.setId(formatId(delete.getDelete().getStatus().getIdStr()));
    actObj.setObjectType("tweet");
    return actObj;
  }

  /**
   * Updates the content, and associated fields, with those from the given tweet
   * @param activity the target of the updates.  Will receive all values from the tweet.
   * @param tweet the object to use as the source
   * @param verb the verb for the given activity's type
   */
  public static void updateActivityContent(Activity activity, Tweet tweet, String verb) {
    activity.setVerb(verb);
    activity.setTitle("");
    if ( tweet != null ) {
      activity.setObject(buildActivityObject(tweet));
      activity.setLinks(getLinks(tweet));
      activity.setContent(tweet.getText());
      addLocationExtension(activity, tweet);
      addTwitterExtensions(activity, tweet);
    }
  }

  /**
   * Gets the links from the Twitter event
   * @param tweet the object to use as the source
   * @return a list of links corresponding to the expanded URL (no t.co)
   */
  public static List<String> getLinks(Tweet tweet) {
    List<String> links = new ArrayList<>();
    if ( tweet.getEntities().getUrls() != null ) {
      for (Url url : tweet.getEntities().getUrls()) {
        links.add(url.getExpandedUrl());
      }
    } else {
      LOGGER.debug(" 0 links");
    }
    return links;
  }

  /**
   * Builds the {@link TargetObject} from the tweet.
   * @param tweet the object to use as the source
   * @return currently returns null for all activities
   */
  public static ActivityObject buildTarget(Tweet tweet) {
    return null;
  }

  /**
   * Adds the location extension and populates with teh twitter data.
   * @param activity the Activity object to update
   * @param tweet the object to use as the source
   */
  public static void addLocationExtension(Activity activity, Tweet tweet) {
    Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);
    Map<String, Object> location = new HashMap<>();
    location.put("id", formatId(
        Optional.ofNullable(Optional.ofNullable(tweet.getIdStr())
            .orElseGet(Optional.of(tweet.getId().toString())::get)).orElse(null)
    ));
    location.put("coordinates", boundingBoxCenter(tweet.getPlace()));
    extensions.put("location", location);
  }

  /**
   * Gets the common twitter {@link Provider} object
   * @return a provider object representing Twitter
   */
  public static Provider getProvider() {
    Provider provider = new Provider();
    provider.setId("id:providers:twitter");
    provider.setObjectType("application");
    provider.setDisplayName("Twitter");
    return provider;
  }

  /**
   * Adds the given Twitter event to the activity as an extension.
   * @param activity the Activity object to update
   * @param event the Twitter event to add as the extension
   */
  public static void addTwitterExtension(Activity activity, ObjectNode event) {
    Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);
    extensions.put("twitter", event);
  }

  /**
   * Formats the ID to conform with the Apache Streams activity ID convention.
   * @param idparts the parts of the ID to join
   * @return a valid Activity ID in format "id:twitter:part1:part2:...partN"
   */
  public static String formatId(String... idparts) {
    return String.join(":", Stream.concat(Arrays.stream(new String[]{"id:twitter"}), Arrays.stream(idparts)).collect(Collectors.toList()));
  }

  /**
   * Takes various parameters from the twitter object that are currently not part of the
   * activity schema and stores them in a generic extensions attribute.
   * @param activity Activity
   * @param tweet Tweet
   */
  public static void addTwitterExtensions(Activity activity, Tweet tweet) {
    Map<String, Object> extensions = ExtensionUtil.getInstance().ensureExtensions(activity);

    List<String> hashtags = new ArrayList<>();
    for (Hashtag hashtag : tweet.getEntities().getHashtags()) {
      hashtags.add(hashtag.getText());
    }
    extensions.put("hashtags", hashtags);

    Map<String, Object> likes = new HashMap<>();
    likes.put("perspectival", tweet.getFavorited());
    likes.put("count", tweet.getFavoriteCount());

    extensions.put("likes", likes);

    Map<String, Object> rebroadcasts = new HashMap<>();
    rebroadcasts.put("perspectival", tweet.getRetweeted());
    rebroadcasts.put("count", tweet.getRetweetCount());

    extensions.put("rebroadcasts", rebroadcasts);

    List<Map<String, Object>> userMentions = new ArrayList<>();
    Entities entities = tweet.getEntities();

    for (UserMentions user : entities.getUserMentions()) {
      //Map the twitter user object into an actor
      Map<String, Object> actor = new HashMap<>();
      actor.put("id", "id:twitter:" + user.getIdStr());
      actor.put("displayName", user.getName());
      actor.put("handle", user.getScreenName());

      userMentions.add(actor);
    }

    extensions.put("user_mentions", userMentions);

    extensions.put("keywords", tweet.getText());
  }

  /**
   * Compute central coordinates from bounding box.
   * @param place the bounding box to use as the source
   */
  public static List<Double> boundingBoxCenter(Place place) {
    if ( place == null ) {
      return new ArrayList<>();
    }
    if ( place.getBoundingBox() == null ) {
      return new ArrayList<>();
    }
    if ( place.getBoundingBox().getCoordinates().size() != 1 ) {
      return new ArrayList<>();
    }
    if ( place.getBoundingBox().getCoordinates().get(0).size() != 4 ) {
      return new ArrayList<>();
    }
    List<Double> lats = new ArrayList<>();
    List<Double> lons = new ArrayList<>();
    for ( List<Double> point : place.getBoundingBox().getCoordinates().get(0)) {
      lats.add(point.get(0));
      lons.add(point.get(1));
    }
    List<Double> result = new ArrayList<>();
    result.add(mean(lats));
    result.add(mean(lons));
    return result;
  }

}
