/* Copyright (c) 2006 Google Inc.
 *
 * 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 sample.gbase.recipe;

import com.google.api.gbase.client.GoogleBaseEntry;
import com.google.api.gbase.client.NumberUnit;
import com.google.gdata.data.Content;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.OtherContent;
import com.google.gdata.data.Person;
import com.google.gdata.data.TextConstruct;
import com.google.gdata.data.TextContent;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * One recipe, ready to be displayed. 
 * Just a data holding object.
 */
public class Recipe {
  public final static String RECIPE_ITEMTYPE = "recipes";
  
  public final static String MAIN_INGREDIENT_ATTRIBUTE = "main ingredient";
  public final static String CUISINE_ATTRIBUTE = "cuisine";
  public final static String COOKING_TIME_ATTRIBUTE = "cooking time";

  public final static String AUTHOR_UNKNOWN = "";
  
  private final String id;
  private final DateTime postedOn;
  private final String postedBy;
  private final NumberUnit<Integer> cookingTime;
  private final String url;
  private final String title;
  private final String description;
  /** A never-null list that contains the main ingredients. */
  private final Set<String> mainIngredient;
  /** A never-null list that contains the cuisines. */
  private final Set<String> cuisine;

  /**
   * Creates a recipe. The parameters can be null.
   * 
   * @param id id generated by the GoogleBase server
   * @param title 
   * @param url alternate url of the recipe; 
   *            if null, when the recipe is used to insert or to update
   *            it is generated by the GoogleBase server
   * @param description
   * @param mainIngredient
   * @param cuisine
   * @param cookingTime
   */
  public Recipe(String id, String title, String url, 
      String description, Set<String> mainIngredient, Set<String> cuisine, 
      NumberUnit<Integer> cookingTime) {
    if (mainIngredient == null) {
      mainIngredient = new HashSet<String>();
    }
    if (cuisine == null) {
      cuisine = new HashSet<String>();
    }
    this.id = id;
    
    this.title = title;
    this.url = url;
    this.description = description;
    this.mainIngredient = mainIngredient;
    this.cuisine = cuisine;
    this.cookingTime = cookingTime;
    
    this.postedOn = null;
    this.postedBy = null;
  }
  
  /**
   * Creates a recipe out of a GoogleBaseEntry.
   * 
   * @param entry an entry that represents a recipe
   */
  public Recipe(GoogleBaseEntry entry) {
    id = extractIdFromUrl(entry.getId());
    title = entry.getTitle().getPlainText();
    url = entry.getHtmlLink().getHref();
    String description = null;
    if (entry.getContent() != null) {
      Content content = entry.getContent();
      if (content instanceof TextContent) {
        description = ((TextContent)content).getContent().getPlainText();
      } else if (content instanceof OtherContent) {
        description = ((OtherContent)content).getText();
      }
    }
    this.description = description;
    mainIngredient = new HashSet<String>(entry.getGoogleBaseAttributes().
        getTextAttributeValues(MAIN_INGREDIENT_ATTRIBUTE));
    cuisine = new HashSet<String>(entry.getGoogleBaseAttributes().
        getTextAttributeValues(CUISINE_ATTRIBUTE));
    cookingTime = entry.getGoogleBaseAttributes().
        getIntUnitAttribute(COOKING_TIME_ATTRIBUTE);
    postedOn = entry.getPublished();

    // if an entry has no author specified, will set it to empty string
    List<Person> authors = entry.getAuthors();
    postedBy = (authors.isEmpty() ? AUTHOR_UNKNOWN : authors.get(0).getName());
  }

  /**
   * Creates an empty recipe, the values are null or empty Sets.
   */
  public Recipe() {
    this(null, null, null, null, null, null, null);
  }

  public GoogleBaseEntry toGoogleBaseEntry(String idUrl)
  {
    GoogleBaseEntry entry = new GoogleBaseEntry();
    entry.getGoogleBaseAttributes().setItemType(RECIPE_ITEMTYPE);
    if (idUrl != null) {
      entry.setId(idUrl);
    }
    entry.setTitle(TextConstruct.create(TextConstruct.Type.TEXT, title, null));
    if (url != null) {
      entry.addHtmlLink(url, null, null);
    }
    if (description != null) {
      // If the original content was not TEXT, the formatting is lost
      entry.setContent(
          TextConstruct.create(TextConstruct.Type.TEXT, description, null));
    }
    for (String ingredient : mainIngredient) {
      entry.getGoogleBaseAttributes().addTextAttribute(
          MAIN_INGREDIENT_ATTRIBUTE, ingredient);
    }
    for (String cuisineItem : cuisine) {
      entry.getGoogleBaseAttributes().addTextAttribute(
          CUISINE_ATTRIBUTE, cuisineItem);
    }
    if (cookingTime != null) {
      entry.getGoogleBaseAttributes().addIntUnitAttribute(
          COOKING_TIME_ATTRIBUTE, cookingTime);
    }
    return entry;
  }
  
  /**
   * Extracts the id of the item from the given url.
   * 
   * The id found in GoogleBaseEntry is a URL that contains the real, numerical
   * id of the item in Google Base. Parsing the URL is unfortunately the
   * only way of getting a numerical id given a GoogleBaseEntry.
   *
   * @param url a URL that ends with "/" [N] number
   */
  private static String extractIdFromUrl(String url) {
    int lastSlash = url.lastIndexOf('/');
    if (lastSlash == -1 || lastSlash == (url.length()-1)) {
      throw new IllegalArgumentException("Id is in a strange format. " + url);
    }
    String oid = url.substring(lastSlash + 1);
    return oid;
  }

  /** Checks whether there is a description for the recipe. */
  public boolean hasDescription() {
    return description != null;
  }

  /** Checks whether there is a cooking time for the recipe. */
  public boolean hasCookingTime() {
    return cookingTime != null;
  }

  /** Checks whether there are some cuisines for the recipe. */
  public boolean hasCuisine() {
    return cuisine.size() > 0;
  }

  /** Checks whether there are some main ingredients for the recipe. */
  public boolean hasMainIngredient() {
    return mainIngredient.size() > 0;
  }

  /** 
   * Gets the date at which the recipe was posted, as a string. 
   * 
   * @param detailed set to true to get a full date and time
   */
  public String getPostedOnAsString(boolean detailed) {
    Date date = new Date(postedOn.getValue());
    String template = detailed ? "MMMMM d, yyyy HH:mm z" : "MMM d";
    DateFormat format = new SimpleDateFormat(template);
    return format.format(date);
  }

  /** Gets the host and protocol from the recipe URL. */
  public String getHostAndProtocol() throws MalformedURLException {
    URL urlObject = new URL(getUrl());
    return urlObject.getProtocol() + "://" + urlObject.getHost();
  }

  /** 
   * Returns true when the title, description, mainIngredient and cuisine 
   * attributes are not null nor empty. 
   */
  public boolean isComplete() {
    return title != null &&
        description != null &&
        mainIngredient.size() > 0 &&
        cuisine.size() > 0;
  }

  @Override
  public String toString() {
    StringBuffer sb = new StringBuffer();
    sb.append("Recipe[");
    appendNamedParameter(sb, "title", title);
    appendNamedParameter(sb, "id", id);
    appendNamedParameter(sb, "url", url);
    appendNamedParameter(sb, "description", description);
    appendNamedParameter(sb, "cookingTime", cookingTime);
    appendNamedParameter(sb, "postedOn", postedOn);
    appendNamedParameter(sb, "postedBy", postedBy);
    appendNamedCollection(sb, "mainIngredient", mainIngredient);
    appendNamedCollection(sb, "cuisine", cuisine);
    sb.append("]");
    return sb.toString();
  }
  
  /**
   * Appends the name and the value of an Object to a StringBuffer.
   * 
   * @param sb
   * @param name
   * @param value
   */
  private static void appendNamedParameter(StringBuffer sb, 
                                     String name, 
                                     Object value) {
    if (value != null) {
      sb.append(name).append("=\"").append(value).append("\" ");
    }
  }

  /**
   * Appends the name and the elements of a Collection to a StringBuffer.
   * 
   * @param sb
   * @param name
   * @param collection
   */
  private static void appendNamedCollection(
      StringBuffer sb, 
      String name,
      Collection<String> collection) {
    if (collection.size() > 0) {
      sb.append(name).append("=(");
      for (String value : collection) {
        sb.append("\"").append(value).append("\", ");
      }
      sb.append(") ");
    }
  }

  /** Gets the id generated by the server. */
  public String getId() {
    return id;
  }

  /** Gets the user assigned title. */
  public String getTitle() {
    return title;
  }

  /**
   * Gets the url of the recipe, as set by the customer. 
   * If the customer sets no url, the server will generate one.
   */
  public String getUrl() {
    return url;
  }

  /** Gets the user assigned description. */
  public String getDescription() {
    return description;
  }

  /** Returns a never-null Set with the main ingredients of the recipe. */
  public Set<String> getMainIngredient() {
    return mainIngredient;
  }

  /** Returns a never-null Set with the cuisines the recipe belongs to. */
  public Set<String> getCuisine() {
    return cuisine;
  }

  /** Gets the user assigned cooking time. */
  public NumberUnit<Integer> getCookingTime() {
    return cookingTime;
  }

  /** Gets the server generated owner attribute of the recipe. */
  public String getPostedBy() {
    return postedBy;
  }

  /** Gets the server generated date at which the recipe was posted. */
  public DateTime getPostedOn() {
    return postedOn;
  }

  /** Returns true when the Recipe doesn't have an id. */
  public boolean isNew() {
    return id == null;
  }
}
