/* 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.client.model;

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

import com.google.gwt.core.client.GWT;

/**
 * A client-side object representing a surface object.
 * 
 * @author knorton@google.com (Kelly Norton)
 */
@SuppressWarnings("serial")
public class Surface implements Serializable {

  /**
   * A callback interface for observing updates on a particular instance of
   * {@link Surface}.
   */
  public static interface Observer {
    void onUpdate(Surface surface);
  }

  private static String[] appendAuthorName(String[] original, String name) {
    final int n = original.length;
    final String[] names = new String[n + 1];
    for (int i = 0; i < n; ++i) {
      names[i] = original[i];
    }
    names[n] = name;
    return names;
  }

  /**
   * The primary key which is always assigned by the server.
   */
  private String key;

  /**
   * The title for the surface.
   */
  private String title;

  /**
   * A list of author nick names.
   */
  private String[] authorNames;

  /**
   * The number of notes on this surface.
   */
  private int noteCount;

  /**
   * A date that is managed by the server which indicates the last time this
   * object was saved on the server.
   */
  private Date lastUpdatedAt;

  /**
   * comma-separated list of author's. This value is a cache of the computation
   * that is done in {@link Surface#computeAuthorNamesAsString(Model)}.
   */
  private transient String authorNamesString;

  /**
   * The list of observers that are listening for updates to this object.
   */
  private transient List<Observer> observers;

  /**
   * Used to create a new surface in client-side code.
   * 
   * @param model
   *          the model to which this surface is bound
   * @param title
   *          the title of the surface
   */
  public Surface(Model model, String title) {
    assert GWT.isClient();
    this.title = title;
    this.authorNames = new String[] { "You" };
    initialize(model);
  }

  /**
   * Constructs a new surface. This constructor can only be invoked on the
   * server.
   * 
   * @param key
   *          the primary key
   * @param title
   *          the surface's title
   * @param authorNames
   *          the names of all the authors
   * @param noteCount
   *          the number of notes on this surface
   * @param lastUpdatedAt
   *          the time the surface was last modified on the server
   */
  public Surface(String key, String title, String[] authorNames, int noteCount,
      Date lastUpdatedAt) {
    assert !GWT.isClient();
    this.key = key;
    this.title = title;
    this.authorNames = authorNames;
    this.noteCount = noteCount;
    this.lastUpdatedAt = lastUpdatedAt;
  }

  /**
   * Needed for RPC serialization.
   */
  @SuppressWarnings("unused")
  private Surface() {
  }

  /**
   * Subscribe an observer to receive update notifications.
   * 
   * @param observer
   */
  public void addObserver(Observer observer) {
    observers.add(observer);
  }

  /**
   * Returns the nick names of all authors on a surface.
   * 
   * @return
   */
  public String[] getAuthorNames() {
    return authorNames;
  }

  /**
   * Returns a comma-separated and human readable list of authors for this
   * surface.
   * 
   * @return
   */
  public String getAuthorNamesAsString() {
    return authorNamesString;
  }

  /**
   * Returns the primary key for this surface.
   * 
   * @return
   */
  public String getKey() {
    return key;
  }

  /**
   * Returns the date corresponding to the time this object was last saved on
   * the server.
   * 
   * @return
   */
  public Date getLastUpdatedAt() {
    return lastUpdatedAt;
  }

  /**
   * Returns the number of notes on this surface.
   * 
   * @return
   */
  public int getNoteCount() {
    return noteCount;
  }

  /**
   * Returns the title for the surface.
   * 
   * @return
   */
  public String getTitle() {
    return title;
  }

  /**
   * Indicates whether the surface has received a key from the server (objects
   * receive keys after they are initially saved to the server).
   * 
   * @return
   */
  public boolean hasKey() {
    return key != null;
  }

  /**
   * Remove an existing observer.
   * 
   * @param observer
   */
  public void removeObserver(Observer observer) {
    observers.remove(observer);
  }

  /**
   * Returns a comma-separated, human-readable string containing all of the
   * authors for this surface. This attempts to shorten author names to
   * something more friendly. For instance, knorton@google.com would be
   * shortened to knorton and Kelly Norton would become Kelly.
   * 
   * @param model
   *          a model that is needed to determine the current user
   * @return
   */
  private String computeAuthorNamesAsString(Model model) {
    final String currentAuthorName = model.getCurrentAuthor().getName();
    final String[] names = authorNames;
    final int n = names.length;

    assert n > 0;

    if (n == 1) {
      return "Just You";
    }

    final StringBuffer buffer = new StringBuffer();

    final int m = n - 1;
    boolean foundYou = false;
    for (int i = 0; i < n; ++i) {
      String name = names[i];
      if (!foundYou && currentAuthorName.equals(name)) {
        name = "You";
        foundYou = true;
      }
      if (i == m) {
        buffer.append(" & ");
      } else if (i != 0) {
        buffer.append(", ");
      }
      buffer.append(Author.getShortName(name));
    }
    return buffer.toString();
  }

  private void notifyUpdate() {
    assert observers != null;
    for (int i = 0, n = observers.size(); i < n; ++i) {
      observers.get(i).onUpdate(this);
    }
  }

  /**
   * Initializes some transient data in the object. This should be called by the
   * {@link Model} when a surface is first introduced in the client application.
   * 
   * @param model
   *          the model to which this object is bound
   */
  void initialize(Model model) {
    assert observers == null;
    authorNamesString = computeAuthorNamesAsString(model);
    observers = new ArrayList<Observer>();
  }

  Surface update(Model model, String authorName, Date updatedAt) {
    assert updatedAt.after(lastUpdatedAt);
    authorNames = appendAuthorName(authorNames, authorName);
    lastUpdatedAt = updatedAt;
    authorNamesString = computeAuthorNamesAsString(model);
    notifyUpdate();
    return this;
  }

  Surface update(Model model, Surface surface) {
    if (!surface.getLastUpdatedAt().equals(lastUpdatedAt)) {
      authorNames = surface.authorNames;
      noteCount = surface.noteCount;
      authorNamesString = computeAuthorNamesAsString(model);
      notifyUpdate();
    }
    return this;
  }

  Surface update(String key, Date updatedAt) {
    this.key = key;
    this.lastUpdatedAt = updatedAt;
    return this;
  }
}
