/*
 *  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 openbook.domain;


import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

/**
 * A persistent entity to demonstrate Master in a Master-Details or Composite pattern for
 * persistent domain model.
 * <br>
 * The Purchase Order - Line Items relationship typically demonstrates a Master-Details pattern.
 * In JPA 2.0, following new features are added to support this common pattern used in domain modeling,
 * and this example demonstrates them.<br>
 * <LI> Compound, Derived identity: This feature allows the Details type to compose its identity from the
 * the Master's identity.
 * <LI> Orphan Delete or Dependent relation: This feature allows to impose composite relation semantics to
 * normally associative relation semantics implied in Java. Composite relation in persistence terms also
 * translates to deletion of Details record from database when the details lose their relation to master.
 * <br>
 * Besides the above two key features, this persistent type also shows usage of
 * <LI>Auto-generated identity.
 * <LI>Enum type persistent attribute.
 * <LI>Date type persistent attribute.
 * <LI>One-to-One uni-directional, immutable mapping to Customer.
 * <LI>One-to-Many bi-directional, immutable mapping to LineItem.
 *
 * @author Pinaki Poddar
 *
 */
@Entity
public class PurchaseOrder implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * Enumerates the status of a Purchase Order.
     *
     */
    public enum Status {PENDING, DELIVERED};

    /**
     * <A name="id">
     * Persistent Identity is generated by the provider.
     */
    @Id
    @GeneratedValue
    private long id;

    @OneToOne(optional=false)
    private Customer customer;

    /**
     * <A name="items">
     * A composite relation.
     * All persistence operation on this order are cascaded to its line items.
     * Moreover, a line item ceases to exist even in the database if it is no
     * more referred to by a order. This is known as <em>orphan delete</em>
     * and can be termed as <em>persistent garbage collection</em>.
     *
     */
    @OneToMany(mappedBy="order", fetch=FetchType.EAGER, cascade=CascadeType.ALL, orphanRemoval=true)
    private List<LineItem> items;

    private Status status;

    private int total;

    @Temporal(TemporalType.TIMESTAMP)
    private Timestamp placedOn;

    @Temporal(TemporalType.TIMESTAMP)
    private Timestamp deliveredOn;

    /**
     * A protected constructor satisfies two purposes:<br>
     * <LI>i)  Status and creation time is set consistently.
     * <LI>ii) OpenJPA Bytecode Enhancer requires an empty constructor for the domain classes.
     * The public constructor of Purchase Order takes a Shopping Cart as argument.
     *
     */
    protected PurchaseOrder() {
        status = Status.PENDING;
        placedOn = new Timestamp(System.currentTimeMillis());
    }

    /**
     * <A name="init"/>
     * Construct a new order by transferring the content of the given {@linkplain ShoppingCart}.
     *
     * @param cart a non-null, non-empty Shopping cart
     * @exception NullPointerException if the cart is null
     * @exception IllegalStateException if the cart is empty
     */
    public PurchaseOrder(ShoppingCart cart) {
        this();
        if (cart == null)
            throw new NullPointerException("Can not create new Purchase Order from null Shopping Cart");
        if (cart.isEmpty())
            throw new IllegalStateException("Can not create new Purchase Order from empty Shopping Cart");
        customer = cart.getCustomer();
        Map<Book, Integer> items = cart.getItems();
        for (Map.Entry<Book, Integer> entry : items.entrySet()) {
            addItem(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Gets the immutable, auto-generated persistent identity of this Purchase Order.
     */
    public long getId() {
        return id;
    }

    /**
     * Gets the customer who placed this Purchase Order.
     *
     * @return immutable Customer.
     */
    public Customer getCustomer() {
        return customer;
    }

    /**
     * Gets the status of this Purchase Order.
     *
     */
    public Status getStatus() {
        return status;
    }

    public boolean isDelivered() {
        return Status.DELIVERED.equals(status);
    }

    /**
     * <A name="setDelivered"/>
     * Sets the status of this Purchase Order as delivered.
     * Setting an order status as delivered nullifies the association to Line Items.
     * Nullifying this association has the important side-effect of Line Item records
     * be deleted from the database because the relation is annotated as orphan delete.
     *
     * @exception IllegalStateException if this order has already been delivered.
     */
    public void setDelivered() {
        if (this.status == Status.DELIVERED)
            throw new IllegalStateException(this + " has been delivered");
        this.status = Status.DELIVERED;
        this.deliveredOn = new Timestamp(System.currentTimeMillis());
        this.items = null;
    }

    /**
     * Gets the items for this Purchase Order.
     *
     * @return the line items of this order. The line items for a delivered order is always null.
     * @see #setDelivered()
     */
    public List<LineItem> getItems() {
        return items;
    }

    /**
     * Adds an item to this Purchase Order.
     * The total is adjusted as a side-effect.
     */
    void addItem(Book book, int quantity) {
        if (book == null)
            throw new NullPointerException("Can not add Line Item to Purchase Order for null Book");
        if (quantity < 1)
            throw new IllegalArgumentException("Can not add Line Item to Purchase Order for negative (=" +
                    quantity + ") number of Book " + book);
        if (items == null)
            items = new ArrayList<>();
        items.add(new LineItem(this, items.size()+1, book, quantity));
        total += (book.getPrice() * quantity);
    }

    /**
     * Gets the total cost of all the items in this order.
     */
    public double getTotal() {
        return total;
    }

    /**
     * Gets the time when this order was placed.
     */
    public Timestamp getPlacedOn() {
        return placedOn;
    }

    /**
     * Gets the time when this order was delivered.
     *
     * @return null if the order has not been delivered yet.
     */
    public Date getDeliveredOn() {
        return deliveredOn;
    }
}
