BookstoreArrayList.java

package com.university.bookstore.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.university.bookstore.api.BookstoreAPI;
import com.university.bookstore.model.Book;

/**
 * ArrayList-based implementation of the BookstoreAPI.
 * 
 * <p>This implementation uses an ArrayList for storage and enforces
 * ISBN uniqueness. All collection returns are defensive copies to
 * maintain encapsulation.</p>
 * 
 * <p>Performance characteristics:</p>
 * <ul>
 *   <li>add: O(n) - due to uniqueness check</li>
 *   <li>removeByIsbn: O(n) - linear search required</li>
 *   <li>findByIsbn: O(n) - linear search required</li>
 *   <li>size: O(1) - ArrayList maintains size</li>
 * </ul>
 * 
 * <p>Note: This implementation is NOT thread-safe. For concurrent access,
 * consider using synchronization or ConcurrentHashMap.</p>
 * 
 * @author Navid Mohaghegh
 * @version 1.0
 * @since 2024-09-15
 */
public class BookstoreArrayList implements BookstoreAPI {
    
    private final List<Book> inventory;
    
    /**
     * Creates a new empty bookstore.
     */
    public BookstoreArrayList() {
        this.inventory = new ArrayList<>();
    }
    
    /**
     * Creates a bookstore with initial books.
     * 
     * @param initialBooks books to add initially (may be null or empty)
     */
    public BookstoreArrayList(Collection<Book> initialBooks) {
        this.inventory = new ArrayList<>();
        if (initialBooks != null) {
            for (Book book : initialBooks) {
                add(book);
            }
        }
    }
    
    @Override
    public boolean add(Book book) {
        if (book == null) {
            return false;
        }
        
        // Check for duplicate ISBN
        if (findByIsbn(book.getIsbn()) != null) {
            return false;
        }
        
        return inventory.add(book);
    }
    
    @Override
    public boolean removeByIsbn(String isbn) {
        if (isbn == null || isbn.trim().isEmpty()) {
            return false;
        }
        
        return inventory.removeIf(book -> book.getIsbn().equals(isbn));
    }
    
    @Override
    public Book findByIsbn(String isbn) {
        if (isbn == null || isbn.trim().isEmpty()) {
            return null;
        }
        
        for (Book book : inventory) {
            if (book.getIsbn().equals(isbn)) {
                return book;
            }
        }
        return null;
    }
    
    @Override
    public List<Book> findByTitle(String titleQuery) {
        if (titleQuery == null || titleQuery.trim().isEmpty()) {
            return Collections.emptyList();
        }
        
        String query = titleQuery.toLowerCase().trim();
        return inventory.stream()
            .filter(book -> book.getTitle().toLowerCase().contains(query))
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Book> findByAuthor(String authorQuery) {
        if (authorQuery == null || authorQuery.trim().isEmpty()) {
            return Collections.emptyList();
        }
        
        String query = authorQuery.toLowerCase().trim();
        return inventory.stream()
            .filter(book -> book.getAuthor().toLowerCase().contains(query))
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Book> findByPriceRange(double minPrice, double maxPrice) {
        if (minPrice < 0 || maxPrice < 0) {
            throw new IllegalArgumentException("Prices cannot be negative");
        }
        if (minPrice > maxPrice) {
            throw new IllegalArgumentException(
                "Minimum price cannot be greater than maximum price");
        }
        
        return inventory.stream()
            .filter(book -> book.getPrice() >= minPrice && book.getPrice() <= maxPrice)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Book> findByYear(int year) {
        return inventory.stream()
            .filter(book -> book.getYear() == year)
            .collect(Collectors.toList());
    }
    
    @Override
    public int size() {
        return inventory.size();
    }
    
    @Override
    public double inventoryValue() {
        return inventory.stream()
            .mapToDouble(Book::getPrice)
            .sum();
    }
    
    @Override
    public Book getMostExpensive() {
        return inventory.stream()
            .max(Comparator.comparingDouble(Book::getPrice))
            .orElse(null);
    }
    
    @Override
    public Book getMostRecent() {
        return inventory.stream()
            .max(Comparator.comparingInt(Book::getYear))
            .orElse(null);
    }
    
    @Override
    public Book[] snapshotArray() {
        return inventory.toArray(new Book[0]);
    }
    
    @Override
    public List<Book> getAllBooks() {
        return new ArrayList<>(inventory);
    }
    
    /**
     * Clears all books from the inventory.
     * Useful for testing and bulk operations.
     */
    public void clear() {
        inventory.clear();
    }
    
    /**
     * Sorts the inventory by title (alphabetical order).
     */
    public void sortByTitle() {
        Collections.sort(inventory);
    }
    
    /**
     * Sorts the inventory by price (ascending).
     */
    public void sortByPrice() {
        inventory.sort(Comparator.comparingDouble(Book::getPrice));
    }
    
    /**
     * Sorts the inventory by year (ascending).
     */
    public void sortByYear() {
        inventory.sort(Comparator.comparingInt(Book::getYear));
    }
    
    /**
     * Gets statistics about the inventory.
     * 
     * @return map with statistics (size, total_value, avg_price, etc.)
     */
    public Map<String, Object> getStatistics() {
        Map<String, Object> stats = new HashMap<>();
        stats.put("size", size());
        stats.put("total_value", inventoryValue());
        stats.put("average_price", inventory.isEmpty() ? 0.0 : inventoryValue() / size());
        
        if (!inventory.isEmpty()) {
            stats.put("min_year", inventory.stream()
                .mapToInt(Book::getYear).min().orElse(0));
            stats.put("max_year", inventory.stream()
                .mapToInt(Book::getYear).max().orElse(0));
            stats.put("unique_authors", inventory.stream()
                .map(Book::getAuthor).distinct().count());
        }
        
        return stats;
    }
    
    @Override
    public String toString() {
        return String.format("BookstoreArrayList[size=%d, value=$%.2f]", 
            size(), inventoryValue());
    }
}