/* Copyright (c) 2009 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 com.google.appengine.demos.sticky.server;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Transaction;
import javax.jdo.annotations.Element;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.User;

/**
 * An application specific Api wrapper around the {@link DatastoreService}.
 * Creating a {@link Store} is relatively expensive so one should be mindful not
 * to create these unnecessarily. For example, in a servlet, the Store should be
 * stored as a field in the store to avoid creating it on every request.
 * 
 * @author knorton@google.com (Kelly Norton)
 * 
 */
public class Store {

  public class Api {

    /**
     * The JDO persistence manager used for all calls.
     */
    private final PersistenceManager manager;

    private Api() {
      manager = factory.getPersistenceManager();
    }

    /**
     * Begin a new transaction.
     * 
     * @return the transaction
     */
    public Transaction begin() {
      final Transaction tx = manager.currentTransaction();
      tx.begin();
      return tx;
    }

    /**
     * Close the connection to the data store. Clients are expected to guarantee
     * that close will be called. This will also rollback any active
     * transaction.
     */
    public void close() {
      final Transaction tx = manager.currentTransaction();
      if (tx.isActive()) {
        tx.rollback();
      }
      manager.close();
    }

    /**
     * Gets the author by email.
     * 
     * @param email
     *          the author's email
     * @return the author
     * @throws JDOObjectNotFoundException
     */
    public Author getAuthor(String email) {
      return manager.getObjectById(Author.class, email);
    }

    /**
     * Gets a note from the data store.
     * 
     * @param key
     *          the note's key
     * @return
     */
    public Note getNote(Key key) {
      return manager.getObjectById(Note.class, key);
    }

    /**
     * Looks in the data store for an author with a matching email. If the
     * author does not exist, a new one will be created. The newly created
     * author will also have access to a newly created surface.
     * 
     * @param user
     *          the user for which an author object is needed
     * @return an author object
     */
    public Author getOrCreateNewAuthor(User user) {
      try {
        return getAuthor(user.getEmail());
      } catch (JDOObjectNotFoundException e) {
        // If an author wasn't found, we create a new one and save it to the
        // store.

        // First, persist a default surface for the author.
        final Transaction txA = begin();
        final Surface surface = new Surface("My First Surface");
        surface.addAuthorName(user.getNickname());
        saveSurface(surface);
        txA.commit();

        // Then, create a new author based on the information in user.
        final Transaction txB = begin();
        final Author author = new Author(user.getEmail(), user.getNickname());
        author.addSurface(surface);
        saveAuthor(author);
        txB.commit();
        return author;
      }
    }

    /**
     * Gets a surface from the data store.
     * 
     * @param key
     *          the surface's key
     * @return
     */
    public Surface getSurface(Key key) {
      return manager.getObjectById(Surface.class, key);
    }

    /**
     * Persist an author to the data store.
     * 
     * @param author
     *          the author to be persisted
     * @return <code>author</code>, for call chaining
     */
    public Author saveAuthor(Author author) {
      return manager.makePersistent(author);
    }

    /**
     * Persist a note to the data store.
     * 
     * @param note
     *          the note to be persisted
     * @return <code>note</code>, for call chaining
     */
    public Note saveNote(Note note) {
      note.lastUpdatedAt = new Date();
      return manager.makePersistent(note);
    }

    /**
     * Persist a surface to the data store.
     * 
     * @param surface
     *          the surface to be persisted
     * @return <code>surface</code>, for call chaining
     */
    public Surface saveSurface(Surface surface) {
      // Update the last updated value before saving.
      surface.lastUpdatedAt = new Date();
      return manager.makePersistent(surface);
    }

    /**
     * Attempts to get the author with the given email. If there is no known
     * author with that email, <code>null</code> will be returned.
     * 
     * @param email
     *          the author's email
     * @return the author or <code>null</code> if the author can't be found
     */
    public Author tryGetAuthor(String email) {
      try {
        return getAuthor(email);
      } catch (JDOObjectNotFoundException e) {
        return null;
      }
    }
  }

  /**
   * An ORM object representing an author.
   */
  @PersistenceCapable(identityType = IdentityType.APPLICATION)
  public static class Author {
    /**
     * The author's email. Serves as the primary key for this object.
     */
    @PrimaryKey
    private String email;

    /**
     * The author's name.
     */
    @Persistent
    private String name;

    /**
     * The keys of all surfaces this author has access to.
     */
    @Persistent(defaultFetchGroup = "true")
    @Element(dependent = "true")
    private List<Key> surfaceKeys = new ArrayList<Key>();

    /**
     * Construct a new author.
     * 
     * @param email
     *          the author's email
     * @param name
     *          the author's name
     */
    public Author(String email, String name) {
      this.name = name;
      setEmail(email);
    }

    /**
     * Give this author access to a surface.
     * 
     * @param surface
     *          the surface the author is being granted access to.
     */
    public void addSurface(Surface surface) {
      final List<Key> keys = new ArrayList<Key>(surfaceKeys);
      keys.add(surface.getKey());
      setSurfaceKeys(keys);
    }

    /**
     * Gets the author's email.
     * 
     * @return
     */
    public String getEmail() {
      return email;
    }

    /**
     * Gets the author's name.
     * 
     * @return
     */
    public String getName() {
      return name;
    }

    /**
     * Returns the keys for each surface that the author has access to.
     * 
     * @return
     */
    public List<Key> getSurfaceKeys() {
      return surfaceKeys;
    }

    /**
     * Returns whether this author has access to a particular surface.
     * 
     * @param surfaceKey
     *          the key of the surface
     * @return <code>true</code> if the author does have access,
     *         <code>false</code> otherwise
     */
    public boolean hasSurface(Key surfaceKey) {
      for (Key key : surfaceKeys) {
        if (key.equals(surfaceKey)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Sets the author's email.
     * 
     * @param email
     */
    public void setEmail(String email) {
      this.email = email;
    }

    /**
     * Sets the author's name.
     * 
     * @param name
     */
    public void setName(String name) {
      this.name = name;
    }

    /**
     * Reassigns the collection of surface keys. This is required to ensure that
     * the ORM will persist element collections.
     * 
     * @param keys
     */
    private void setSurfaceKeys(List<Key> keys) {
      surfaceKeys = keys;
    }
  }

  /**
   * An ORM object representing a note.
   */
  @PersistenceCapable(identityType = IdentityType.APPLICATION)
  public static class Note {
    /**
     * An auto-generated primary key for this object. This key will be a child
     * key of the owning surface's key.
     */
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    /**
     * The x position of the note.
     */
    @Persistent
    private int x;

    /**
     * The y position of the note.
     */
    @Persistent
    private int y;

    /**
     * The width of the note.
     * 
     * <p>
     * NOTE: The application does not currently provide the ability to resize
     * notes.
     * </p>
     */
    @Persistent
    private int width;

    /**
     * The height of the note
     * 
     * <p>
     * NOTE: The application does not currently provide the ability to resize
     * notes.
     * </p>
     */
    @Persistent
    private int height;

    /**
     * The text content of the note.
     */
    @Persistent
    private String content;

    /**
     * The date of the last time this object was persisted.
     */
    @Persistent
    private Date lastUpdatedAt = new Date();

    /**
     * The email of the author created this note.
     */
    @Persistent
    private String authorEmail;

    /**
     * The name of the author who created this note.
     */
    @Persistent
    private String authorName;

    /**
     * Create a new note.
     * 
     * @param owner
     *          the author who created this note
     * @param x
     * @param y
     * @param width
     * @param height
     */
    public Note(Author owner, int x, int y, int width, int height) {
      this.x = x;
      this.y = y;
      this.width = width;
      this.height = height;
      authorEmail = owner.getEmail();
      authorName = owner.getName();
    }

    /**
     * The author's email.
     * 
     * @return
     */
    public String getAuthorEmail() {
      return authorEmail;
    }

    /**
     * The author's name.
     * 
     * @return
     */
    public String getAuthorName() {
      return authorName;
    }

    /**
     * The text of the note. This value is not escaped in anyway and should
     * never be used as html on the client.
     * 
     * @return unsafe text content
     */
    public String getContent() {
      return content;
    }

    /**
     * Gets the height of the note.
     * 
     * @return
     */
    public int getHeight() {
      return height;
    }

    /**
     * Gets the object's primary key.
     * 
     * @return
     */
    public Key getKey() {
      return key;
    }

    /**
     * Gets the date of the last time this object was persisted.
     * 
     * @return
     */
    public Date getLastUpdatedAt() {
      return lastUpdatedAt;
    }

    /**
     * Gets the width of the note.
     * 
     * @return
     */
    public int getWidth() {
      return width;
    }

    /**
     * Gets the x position of the note.
     * 
     * @return
     */
    public int getX() {
      return x;
    }

    /**
     * Gets the y position of the note.
     * 
     * @return
     */
    public int getY() {
      return y;
    }

    /**
     * Indicates whether the given author is the owner of this note.
     * 
     * @param author
     *          the author
     * @return <code>true</code> if <code>author</code> is the owner of the
     *         note, <code>false</code> otherwise.
     */
    public boolean isOwnedBy(Author author) {
      return author.getEmail().equals(authorEmail);
    }

    /**
     * Sets the content.
     * 
     * @param content
     */
    public void setContent(String content) {
      this.content = content;
    }

    /**
     * Sets the height of the note.
     * 
     * @param height
     */
    public void setHeight(int height) {
      this.height = height;
    }

    /**
     * Sets the width of the note.
     * 
     * @param width
     */
    public void setWidth(int width) {
      this.width = width;
    }

    /**
     * Sets the x position of the note.
     * 
     * @param x
     */
    public void setX(int x) {
      this.x = x;
    }

    /**
     * Sets the y position of the note.
     * 
     * @param y
     */
    public void setY(int y) {
      this.y = y;
    }
  }

  /**
   * A JDO object representing a surface.
   */
  @PersistenceCapable(identityType = IdentityType.APPLICATION)
  public static class Surface {
    /**
     * An auto-generated primary key.
     */
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    /**
     * The title of the surface.
     */
    @Persistent
    private String title;

    /**
     * The date of the last time this surface was persisted.
     */
    @Persistent
    private Date lastUpdatedAt;

    /**
     * The name of each author that has access to this surface.
     */
    @Persistent(defaultFetchGroup = "true")
    private List<String> authorNames = new ArrayList<String>();

    /**
     * The notes that are stuck to this surface.
     */
    @Element(dependent = "true")
    private List<Note> notes = new ArrayList<Note>();

    /**
     * Create a new surface.
     * 
     * @param title
     */
    public Surface(String title) {
      this.title = title;
    }

    /**
     * Add the name to the author names. Calls to this method are generally
     * paired with a call to {@link Author#addSurface(Surface)}.
     * 
     * @param name
     */
    public void addAuthorName(String name) {
      final List<String> names = new ArrayList<String>(authorNames);
      names.add(name);
      setAuthorNames(names);
    }

    /**
     * Gets the collection of author names.
     * 
     * @return
     */
    public List<String> getAuthorNames() {
      return authorNames;
    }

    /**
     * Gets the primary key for this object.
     * 
     * @return
     */
    public Key getKey() {
      return key;
    }

    /**
     * Gets the date of the last time this object was persisted.
     * 
     * @return
     */
    public Date getLastUpdatedAt() {
      return lastUpdatedAt;
    }

    /**
     * Gets all the notes that are stuck to this surface.
     * 
     * @return
     */
    public List<Note> getNotes() {
      return notes;
    }

    /**
     * Get the surface's title.
     * 
     * @return
     */
    public String getTitle() {
      return title;
    }

    /**
     * Sets the surface's title.
     * 
     * @param title
     */
    public void setTitle(String title) {
      this.title = title;
    }

    /**
     * Reassigns the collection of author names. This is required to ensure that
     * the ORM persists element collections.
     * 
     * @param names
     */
    private void setAuthorNames(List<String> names) {
      authorNames = names;
    }
  }

  private final PersistenceManagerFactory factory;

  /**
   * Create a new Store based on a particular config.
   * 
   * @param config
   */
  public Store(String config) {
    this.factory = JDOHelper.getPersistenceManagerFactory(config);
  }

  /**
   * Starts a data store session and returns an Api object to use.
   * 
   * @return
   */
  public Api getApi() {
    return new Api();
  }
}
