/*
 * Copyright 2010 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.gwt.sample.showcase.client.content.cell;

import com.google.gwt.core.client.GWT;
import com.google.gwt.i18n.client.Constants;
import com.google.gwt.user.client.Random;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.ProvidesKey;

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

/**
 * The data source for contact information used in the sample.
 */
public class ContactDatabase {

  /**
   * A contact category.
   */
  public static class Category {

    private final String displayName;

    private Category(String displayName) {
      this.displayName = displayName;
    }

    public String getDisplayName() {
      return displayName;
    }
  }

  /**
   * Information about a contact.
   */
  public static class ContactInfo implements Comparable<ContactInfo> {

    /**
     * The key provider that provides the unique ID of a contact.
     */
    public static final ProvidesKey<ContactInfo> KEY_PROVIDER = new ProvidesKey<
        ContactInfo>() {
      public Object getKey(ContactInfo item) {
        return item == null ? null : item.getId();
      }
    };

    private static int nextId = 0;

    private String address;
    private Date birthday;
    private Category category;
    private String firstName;
    private final int id;
    private String lastName;

    public ContactInfo(Category category) {
      this.id = nextId;
      nextId++;
      setCategory(category);
    }

    public int compareTo(ContactInfo o) {
      return (o == null || o.firstName == null) ? -1 : -o.firstName.compareTo(
          firstName);
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof ContactInfo) {
        return id == ((ContactInfo) o).id;
      }
      return false;
    }

    /**
     * @return the contact's address
     */
    public String getAddress() {
      return address;
    }

    /**
     * @return the contact's birthday
     */
    public Date getBirthday() {
      return birthday;
    }

    /**
     * @return the category of the conteact
     */
    public Category getCategory() {
      return category;
    }

    /**
     * @return the contact's firstName
     */
    public String getFirstName() {
      return firstName;
    }

    /**
     * @return the contact's full name
     */
    public final String getFullName() {
      return firstName + " " + lastName;
    }

    /**
     * @return the unique ID of the contact
     */
    public int getId() {
      return this.id;
    }

    /**
     * @return the contact's lastName
     */
    public String getLastName() {
      return lastName;
    }

    @Override
    public int hashCode() {
      return id;
    }

    /**
     * Set the contact's address.
     *
     * @param address the address
     */
    public void setAddress(String address) {
      this.address = address;
    }

    /**
     * Set the contact's birthday.
     *
     * @param birthday the birthday
     */
    public void setBirthday(Date birthday) {
      this.birthday = birthday;
    }

    /**
     * Set the contact's category.
     *
     * @param category the category to set
     */
    public void setCategory(Category category) {
      assert category != null : "category cannot be null";
      this.category = category;
    }

    /**
     * Set the contact's first name.
     *
     * @param firstName the firstName to set
     */
    public void setFirstName(String firstName) {
      this.firstName = firstName;
    }

    /**
     * Set the contact's last name.
     *
     * @param lastName the lastName to set
     */
    public void setLastName(String lastName) {
      this.lastName = lastName;
    }
  }

  /**
   * The constants used in this Content Widget.
   */
  static interface DatabaseConstants extends Constants {
    String[] contactDatabaseCategories();
  }

  private static final String[] FEMALE_FIRST_NAMES = {
      "Mary", "Patricia", "Linda", "Barbara", "Elizabeth", "Jennifer", "Maria",
      "Susan", "Margaret", "Dorothy", "Lisa", "Nancy", "Karen", "Betty",
      "Helen", "Sandra", "Donna", "Carol", "Ruth", "Sharon", "Michelle",
      "Laura", "Sarah", "Kimberly", "Deborah", "Jessica", "Shirley", "Cynthia",
      "Angela", "Melissa", "Brenda", "Amy", "Anna", "Rebecca", "Virginia",
      "Kathleen", "Pamela", "Martha", "Debra", "Amanda", "Stephanie", "Carolyn",
      "Christine", "Marie", "Janet", "Catherine", "Frances", "Ann", "Joyce",
      "Diane", "Alice", "Julie", "Heather", "Teresa", "Doris", "Gloria",
      "Evelyn", "Jean", "Cheryl", "Mildred", "Katherine", "Joan", "Ashley",
      "Judith", "Rose", "Janice", "Kelly", "Nicole", "Judy", "Christina",
      "Kathy", "Theresa", "Beverly", "Denise", "Tammy", "Irene", "Jane", "Lori",
      "Rachel", "Marilyn", "Andrea", "Kathryn", "Louise", "Sara", "Anne",
      "Jacqueline", "Wanda", "Bonnie", "Julia", "Ruby", "Lois", "Tina",
      "Phyllis", "Norma", "Paula", "Diana", "Annie", "Lillian", "Emily",
      "Robin", "Peggy", "Crystal", "Gladys", "Rita", "Dawn", "Connie",
      "Florence", "Tracy", "Edna", "Tiffany", "Carmen", "Rosa", "Cindy",
      "Grace", "Wendy", "Victoria", "Edith", "Kim", "Sherry", "Sylvia",
      "Josephine", "Thelma", "Shannon", "Sheila", "Ethel", "Ellen", "Elaine",
      "Marjorie", "Carrie", "Charlotte", "Monica", "Esther", "Pauline", "Emma",
      "Juanita", "Anita", "Rhonda", "Hazel", "Amber", "Eva", "Debbie", "April",
      "Leslie", "Clara", "Lucille", "Jamie", "Joanne", "Eleanor", "Valerie",
      "Danielle", "Megan", "Alicia", "Suzanne", "Michele", "Gail", "Bertha",
      "Darlene", "Veronica", "Jill", "Erin", "Geraldine", "Lauren", "Cathy",
      "Joann", "Lorraine", "Lynn", "Sally", "Regina", "Erica", "Beatrice",
      "Dolores", "Bernice", "Audrey", "Yvonne", "Annette", "June", "Samantha",
      "Marion", "Dana", "Stacy", "Ana", "Renee", "Ida", "Vivian", "Roberta",
      "Holly", "Brittany", "Melanie", "Loretta", "Yolanda", "Jeanette",
      "Laurie", "Katie", "Kristen", "Vanessa", "Alma", "Sue", "Elsie", "Beth",
      "Jeanne"};
  private static final String[] MALE_FIRST_NAMES = {
      "James", "John", "Robert", "Michael", "William", "David", "Richard",
      "Charles", "Joseph", "Thomas", "Christopher", "Daniel", "Paul", "Mark",
      "Donald", "George", "Kenneth", "Steven", "Edward", "Brian", "Ronald",
      "Anthony", "Kevin", "Jason", "Matthew", "Gary", "Timothy", "Jose",
      "Larry", "Jeffrey", "Frank", "Scott", "Eric", "Stephen", "Andrew",
      "Raymond", "Gregory", "Joshua", "Jerry", "Dennis", "Walter", "Patrick",
      "Peter", "Harold", "Douglas", "Henry", "Carl", "Arthur", "Ryan", "Roger",
      "Joe", "Juan", "Jack", "Albert", "Jonathan", "Justin", "Terry", "Gerald",
      "Keith", "Samuel", "Willie", "Ralph", "Lawrence", "Nicholas", "Roy",
      "Benjamin", "Bruce", "Brandon", "Adam", "Harry", "Fred", "Wayne", "Billy",
      "Steve", "Louis", "Jeremy", "Aaron", "Randy", "Howard", "Eugene",
      "Carlos", "Russell", "Bobby", "Victor", "Martin", "Ernest", "Phillip",
      "Todd", "Jesse", "Craig", "Alan", "Shawn", "Clarence", "Sean", "Philip",
      "Chris", "Johnny", "Earl", "Jimmy", "Antonio", "Danny", "Bryan", "Tony",
      "Luis", "Mike", "Stanley", "Leonard", "Nathan", "Dale", "Manuel",
      "Rodney", "Curtis", "Norman", "Allen", "Marvin", "Vincent", "Glenn",
      "Jeffery", "Travis", "Jeff", "Chad", "Jacob", "Lee", "Melvin", "Alfred",
      "Kyle", "Francis", "Bradley", "Jesus", "Herbert", "Frederick", "Ray",
      "Joel", "Edwin", "Don", "Eddie", "Ricky", "Troy", "Randall", "Barry",
      "Alexander", "Bernard", "Mario", "Leroy", "Francisco", "Marcus",
      "Micheal", "Theodore", "Clifford", "Miguel", "Oscar", "Jay", "Jim", "Tom",
      "Calvin", "Alex", "Jon", "Ronnie", "Bill", "Lloyd", "Tommy", "Leon",
      "Derek", "Warren", "Darrell", "Jerome", "Floyd", "Leo", "Alvin", "Tim",
      "Wesley", "Gordon", "Dean", "Greg", "Jorge", "Dustin", "Pedro", "Derrick",
      "Dan", "Lewis", "Zachary", "Corey", "Herman", "Maurice", "Vernon",
      "Roberto", "Clyde", "Glen", "Hector", "Shane", "Ricardo", "Sam", "Rick",
      "Lester", "Brent", "Ramon", "Charlie", "Tyler", "Gilbert", "Gene"};
  private static final String[] LAST_NAMES = {
      "Smith", "Johnson", "Williams", "Jones", "Brown", "Davis", "Miller",
      "Wilson", "Moore", "Taylor", "Anderson", "Thomas", "Jackson", "White",
      "Harris", "Martin", "Thompson", "Garcia", "Martinez", "Robinson", "Clark",
      "Rodriguez", "Lewis", "Lee", "Walker", "Hall", "Allen", "Young",
      "Hernandez", "King", "Wright", "Lopez", "Hill", "Scott", "Green", "Adams",
      "Baker", "Gonzalez", "Nelson", "Carter", "Mitchell", "Perez", "Roberts",
      "Turner", "Phillips", "Campbell", "Parker", "Evans", "Edwards", "Collins",
      "Stewart", "Sanchez", "Morris", "Rogers", "Reed", "Cook", "Morgan",
      "Bell", "Murphy", "Bailey", "Rivera", "Cooper", "Richardson", "Cox",
      "Howard", "Ward", "Torres", "Peterson", "Gray", "Ramirez", "James",
      "Watson", "Brooks", "Kelly", "Sanders", "Price", "Bennett", "Wood",
      "Barnes", "Ross", "Henderson", "Coleman", "Jenkins", "Perry", "Powell",
      "Long", "Patterson", "Hughes", "Flores", "Washington", "Butler",
      "Simmons", "Foster", "Gonzales", "Bryant", "Alexander", "Russell",
      "Griffin", "Diaz", "Hayes", "Myers", "Ford", "Hamilton", "Graham",
      "Sullivan", "Wallace", "Woods", "Cole", "West", "Jordan", "Owens",
      "Reynolds", "Fisher", "Ellis", "Harrison", "Gibson", "Mcdonald", "Cruz",
      "Marshall", "Ortiz", "Gomez", "Murray", "Freeman", "Wells", "Webb",
      "Simpson", "Stevens", "Tucker", "Porter", "Hunter", "Hicks", "Crawford",
      "Henry", "Boyd", "Mason", "Morales", "Kennedy", "Warren", "Dixon",
      "Ramos", "Reyes", "Burns", "Gordon", "Shaw", "Holmes", "Rice",
      "Robertson", "Hunt", "Black", "Daniels", "Palmer", "Mills", "Nichols",
      "Grant", "Knight", "Ferguson", "Rose", "Stone", "Hawkins", "Dunn",
      "Perkins", "Hudson", "Spencer", "Gardner", "Stephens", "Payne", "Pierce",
      "Berry", "Matthews", "Arnold", "Wagner", "Willis", "Ray", "Watkins",
      "Olson", "Carroll", "Duncan", "Snyder", "Hart", "Cunningham", "Bradley",
      "Lane", "Andrews", "Ruiz", "Harper", "Fox", "Riley", "Armstrong",
      "Carpenter", "Weaver", "Greene", "Lawrence", "Elliott", "Chavez", "Sims",
      "Austin", "Peters", "Kelley", "Franklin", "Lawson"};
  private static final String[] STREET_NAMES = {
      "Peachtree", "First", "Second", "Third", "Fourth", "Fifth", "Sixth",
      "Tenth", "Fourteenth", "Spring", "Techwood", "West Peachtree", "Juniper",
      "Cypress", "Fowler", "Piedmont", "Juniper", "Main", "Central", "Currier",
      "Courtland", "Williams", "Centennial", "Olympic", "Baker", "Highland",
      "Pryor", "Decatur", "Bell", "Edgewood", "Mitchell", "Forsyth", "Capital"};
  private static final String[] STREET_SUFFIX = {
      "St", "Rd", "Ln", "Blvd", "Way", "Pkwy", "Cir", "Ave"};

  /**
   * The singleton instance of the database.
   */
  private static ContactDatabase instance;

  /**
   * Get the singleton instance of the contact database.
   *
   * @return the singleton instance
   */
  public static ContactDatabase get() {
    if (instance == null) {
      instance = new ContactDatabase();
    }
    return instance;
  }

  /**
   * The provider that holds the list of contacts in the database.
   */
  private ListDataProvider<ContactInfo> dataProvider = new ListDataProvider<
      ContactInfo>();

  private final Category[] categories;

  /**
   * Construct a new contact database.
   */
  private ContactDatabase() {
    // Initialize the categories.
    DatabaseConstants constants = GWT.create(DatabaseConstants.class);
    String[] catNames = constants.contactDatabaseCategories();
    categories = new Category[catNames.length];
    for (int i = 0; i < catNames.length; i++) {
      categories[i] = new Category(catNames[i]);
    }

    // Generate initial data.
    generateContacts(250);
  }

  /**
   * Add a new contact.
   *
   * @param contact the contact to add.
   */
  public void addContact(ContactInfo contact) {
    List<ContactInfo> contacts = dataProvider.getList();
    // Remove the contact first so we don't add a duplicate.
    contacts.remove(contact);
    contacts.add(contact);
  }

  /**
   * Add a display to the database. The current range of interest of the display
   * will be populated with data.
   *
   * @param display a {@Link HasData}.
   */
  public void addDataDisplay(HasData<ContactInfo> display) {
    dataProvider.addDataDisplay(display);
  }

  /**
   * Generate the specified number of contacts and add them to the data
   * provider.
   *
   * @param count the number of contacts to generate.
   */
  public void generateContacts(int count) {
    List<ContactInfo> contacts = dataProvider.getList();
    for (int i = 0; i < count; i++) {
      contacts.add(createContactInfo());
    }
  }

  /**
   * Get the categories in the database.
   *
   * @return the categories in the database
   */
  public Category[] queryCategories() {
    return categories;
  }

  /**
   * Query all contacts for the specified category.
   *
   * @param category the category
   * @return the list of contacts in the category
   */
  public List<ContactInfo> queryContactsByCategory(Category category) {
    List<ContactInfo> matches = new ArrayList<ContactInfo>();
    for (ContactInfo contact : dataProvider.getList()) {
      if (contact.getCategory() == category) {
        matches.add(contact);
      }
    }
    return matches;
  }

  /**
   * Query all contacts for the specified category that begin with the specified
   * first name prefix.
   *
   * @param category the category
   * @param firstNamePrefix the prefix of the first name
   * @return the list of contacts in the category
   */
  public List<ContactInfo> queryContactsByCategoryAndFirstName(
      Category category, String firstNamePrefix) {
    List<ContactInfo> matches = new ArrayList<ContactInfo>();
    for (ContactInfo contact : dataProvider.getList()) {
      if (contact.getCategory() == category
          && contact.getFirstName().startsWith(firstNamePrefix)) {
        matches.add(contact);
      }
    }
    return matches;
  }

  /**
   * Refresh all displays.
   */
  public void refreshDisplays() {
    dataProvider.refresh();
  }

  /**
   * Create a new random {@link ContactInfo}.
   *
   * @return the new {@link ContactInfo}.
   */
  @SuppressWarnings("deprecation")
  private ContactInfo createContactInfo() {
    ContactInfo contact = new ContactInfo(nextValue(categories));
    contact.setLastName(nextValue(LAST_NAMES));
    if (Random.nextBoolean()) {
      // Male.
      contact.setFirstName(nextValue(MALE_FIRST_NAMES));
    } else {
      // Female.
      contact.setFirstName(nextValue(FEMALE_FIRST_NAMES));
    }

    // Create a birthday between 20-80 years ago.
    int year = (new Date()).getYear() - 21 - Random.nextInt(61);
    contact.setBirthday(
        new Date(year, Random.nextInt(12), 1 + Random.nextInt(31)));

    // Create an address.
    int addrNum = 1 + Random.nextInt(999);
    String addrStreet = nextValue(STREET_NAMES);
    String addrSuffix = nextValue(STREET_SUFFIX);
    contact.setAddress(addrNum + " " + addrStreet + " " + addrSuffix);
    return contact;
  }

  /**
   * Get the next random value from an array.
   *
   * @param array the array
   * @return a random value in the array
   */
  private <T> T nextValue(T[] array) {
    return array[Random.nextInt(array.length)];
  }

}