/*
 * 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
 *
 *   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.rss.serializer;

import org.apache.streams.data.ActivitySerializer;
import org.apache.streams.data.util.RFC3339Utils;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.pojo.json.Activity;
import org.apache.streams.pojo.json.ActivityObject;
import org.apache.streams.pojo.json.Author;
import org.apache.streams.pojo.json.Provider;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

public class SyndEntryActivitySerializer implements ActivitySerializer<ObjectNode> {

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

  private boolean includeRomeExtension;

  public SyndEntryActivitySerializer() {
    this(true);
  }

  public SyndEntryActivitySerializer(boolean includeRomeExtension) {
    this.includeRomeExtension = includeRomeExtension;
  }

  @Override
  public List<Activity> deserializeAll(List<ObjectNode> objectNodes) {
    List<Activity> result = new LinkedList<>();
    for (ObjectNode node : objectNodes) {
      result.add(deserialize(node));
    }
    return result;
  }

  @Override
  public String serializationFormat() {
    return "application/streams-provider-rss";
  }

  @Override
  public ObjectNode serialize(Activity deserialized) {
    throw new UnsupportedOperationException("Cannot currently serialize to Rome");
  }

  @Override
  public Activity deserialize(ObjectNode syndEntry) {
    return deserializeWithRomeExtension(syndEntry, this.includeRomeExtension);
  }

  /**
   * deserializeWithRomeExtension ObjectNode entry withExtension.
   * @param entry ObjectNode
   * @param withExtension whether to add Rome Extension
   * @return Activity
   */
  public Activity deserializeWithRomeExtension(ObjectNode entry, boolean withExtension) {
    Objects.requireNonNull(entry);

    Activity activity = new Activity();
    Provider provider = buildProvider(entry);
    ActivityObject actor = buildActor(entry);
    ActivityObject activityObject = buildActivityObject(entry);

    activityObject.setUrl(provider.getUrl());
    activityObject.setAuthor(actor.getAuthor());

    activity.setUrl(provider.getUrl());
    activity.setProvider(provider);
    activity.setActor(actor);
    activity.setVerb("post");
    activity.setId("id:rss:post:" + activity.getUrl());

    JsonNode published = entry.get("publishedDate");
    if (published != null) {
      try {
        activity.setPublished(RFC3339Utils.parseToUTC(published.textValue()));
      } catch (Exception ex) {
        LOGGER.warn("Failed to parse date : {}", published.textValue());

        DateTime now = DateTime.now().withZone(DateTimeZone.UTC);
        activity.setPublished(now);
      }
    }

    activity.setUpdated(activityObject.getUpdated());
    activity.setObject(activityObject);

    if (withExtension) {
      activity = addRomeExtension(activity, entry);
    }

    return activity;
  }

  /**
   * Given an RSS entry, extra out the author and actor information and return it
   * in an actor object
   *
   * @param entry entry
   * @return $.actor
   */
  private ActivityObject buildActor(ObjectNode entry) {
    ActivityObject actor = new ActivityObject();
    Author author = new Author();

    if (entry.get("author") != null) {
      author.setId(entry.get("author").textValue());
      author.setDisplayName(entry.get("author").textValue());

      actor.setAuthor(author);
      String uriToSet = entry.get("rssFeed") != null ? entry.get("rssFeed").asText() : null;

      actor.setId("id:rss:" + uriToSet + ":" + author.getId());
      actor.setDisplayName(author.getDisplayName());
    }

    return actor;
  }

  /**
   * Given an RSS object, build the ActivityObject.
   *
   * @param entry ObjectNode
   * @return $.object
   */
  private ActivityObject buildActivityObject(ObjectNode entry) {
    ActivityObject activityObject = new ActivityObject();

    JsonNode summary = entry.get("description");
    if (summary != null) {
      activityObject.setSummary(summary.textValue());
    } else if ((summary = entry.get("title")) != null) {
      activityObject.setSummary(summary.textValue());
    }

    return activityObject;
  }

  /**
   * Given an RSS object, build and return the Provider object.
   *
   * @param entry ObjectNode
   * @return $.provider
   */
  private Provider buildProvider(ObjectNode entry) {
    Provider provider = new Provider();

    String link = null;
    String uri = null;
    String resourceLocation = null;

    if (entry.get("link") != null) {
      link = entry.get("link").textValue();
    }
    if (entry.get("uri") != null) {
      uri = entry.get("uri").textValue();
    }
    /*
     * Order of precedence for resourceLocation selection
     *
     * 1. Valid URI
     * 2. Valid Link
     * 3. Non-null URI
     * 4. Non-null Link
     */
    if (isValidResource(uri)) {
      resourceLocation = uri;
    } else if (isValidResource(link)) {
      resourceLocation = link;
    } else if (uri != null || link != null) {
      resourceLocation = (uri != null) ? uri : link;
    }

    provider.setId("id:providers:rss");
    provider.setUrl(resourceLocation);
    provider.setDisplayName("RSS");

    return provider;
  }

  /**
   * Tests whether or not the passed in resource is a valid URI.
   * @param resource resource
   * @return boolean of whether or not the resource is valid
   */
  private boolean isValidResource(String resource) {
    return resource != null && (resource.startsWith("http") || resource.startsWith("www"));
  }

  /**
   * Given an RSS object and an existing activity,
   * add the Rome extension to that activity and return it.
   *
   * @param activity Activity
   * @param entry ObjectNode
   * @return Activity
   */
  private Activity addRomeExtension(Activity activity, ObjectNode entry) {
    ObjectMapper mapper = StreamsJacksonMapper.getInstance();
    ObjectNode activityRoot = mapper.convertValue(activity, ObjectNode.class);
    ObjectNode extensions = JsonNodeFactory.instance.objectNode();

    extensions.put("rome", entry);
    activityRoot.put("extensions", extensions);

    activity = mapper.convertValue(activityRoot, Activity.class);

    return activity;
  }
}
