MaterialStoreImpl.java

package com.university.bookstore.impl;

import java.util.ArrayList;
import java.util.Arrays;
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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.university.bookstore.api.MaterialStore;
import com.university.bookstore.model.Magazine;
import com.university.bookstore.model.Material;
import com.university.bookstore.model.Media;
import com.university.bookstore.model.PrintedBook;

/**
 * Implementation of MaterialStore using ArrayList with polymorphic handling.
 * Demonstrates polymorphism, SOLID principles, and defensive programming.
 * 
 * @author Navid Mohaghegh
 * @version 2.0
 * @since 2024-09-15
 */
public class MaterialStoreImpl implements MaterialStore {
    
    private final List<Material> inventory;
    private final Map<String, Material> idIndex;
    
    /**
     * Creates a new empty material store.
     */
    public MaterialStoreImpl() {
        this.inventory = new ArrayList<>();
        this.idIndex = new HashMap<>();
    }
    
    /**
     * Creates a material store with initial materials.
     * 
     * @param initialMaterials materials to add initially
     */
    public MaterialStoreImpl(Collection<Material> initialMaterials) {
        this();
        if (initialMaterials != null) {
            for (Material material : initialMaterials) {
                addMaterial(material);
            }
        }
    }
    
    @Override
    public synchronized boolean addMaterial(Material material) {
        if (material == null) {
            throw new NullPointerException("Cannot add null material");
        }
        
        if (idIndex.containsKey(material.getId())) {
            return false;
        }
        
        inventory.add(material);
        idIndex.put(material.getId(), material);
        return true;
    }
    
    @Override
    public synchronized Optional<Material> removeMaterial(String id) {
        if (id == null) {
            return Optional.empty();
        }
        
        Material material = idIndex.remove(id);
        if (material != null) {
            inventory.remove(material);
            return Optional.of(material);
        }
        return Optional.empty();
    }
    
    @Override
    public Optional<Material> findById(String id) {
        if (id == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(idIndex.get(id));
    }
    
    @Override
    public List<Material> searchByTitle(String title) {
        if (title == null || title.trim().isEmpty()) {
            return new ArrayList<>();
        }
        
        String searchTerm = title.toLowerCase().trim();
        return inventory.stream()
            .filter(m -> m.getTitle().toLowerCase().contains(searchTerm))
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> searchByCreator(String creator) {
        if (creator == null || creator.trim().isEmpty()) {
            return new ArrayList<>();
        }
        
        String searchTerm = creator.toLowerCase().trim();
        return inventory.stream()
            .filter(m -> m.getCreator().toLowerCase().contains(searchTerm))
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> getMaterialsByType(Material.MaterialType type) {
        if (type == null) {
            return new ArrayList<>();
        }
        
        return inventory.stream()
            .filter(m -> m.getType() == type)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Media> getMediaMaterials() {
        return inventory.stream()
            .filter(m -> m instanceof Media)
            .map(m -> (Media) m)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> filterMaterials(Predicate<Material> predicate) {
        if (predicate == null) {
            throw new NullPointerException("Predicate cannot be null");
        }
        
        return inventory.stream()
            .filter(predicate)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> getMaterialsByPriceRange(double minPrice, double maxPrice) {
        if (minPrice < 0 || maxPrice < 0 || minPrice > maxPrice) {
            return new ArrayList<>();
        }
        
        return inventory.stream()
            .filter(m -> m.getPrice() >= minPrice && m.getPrice() <= maxPrice)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> getMaterialsByYear(int year) {
        return inventory.stream()
            .filter(m -> m.getYear() == year)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> getAllMaterialsSorted() {
        List<Material> sorted = new ArrayList<>(inventory);
        Collections.sort(sorted);
        return sorted;
    }
    
    @Override
    public List<Material> getAllMaterials() {
        return new ArrayList<>(inventory);
    }
    
    @Override
    public double getTotalInventoryValue() {
        return inventory.stream()
            .mapToDouble(Material::getPrice)
            .sum();
    }
    
    @Override
    public double getTotalDiscountedValue() {
        return inventory.stream()
            .mapToDouble(Material::getDiscountedPrice)
            .sum();
    }
    
    @Override
    public InventoryStats getInventoryStats() {
        if (inventory.isEmpty()) {
            return new InventoryStats(0, 0, 0, 0, 0, 0);
        }
        
        List<Double> prices = inventory.stream()
            .map(Material::getPrice)
            .sorted()
            .collect(Collectors.toList());
        
        double averagePrice = prices.stream()
            .mapToDouble(Double::doubleValue)
            .average()
            .orElse(0.0);
        
        double medianPrice = prices.size() % 2 == 0
            ? (prices.get(prices.size() / 2 - 1) + prices.get(prices.size() / 2)) / 2
            : prices.get(prices.size() / 2);
        
        int uniqueTypes = (int) inventory.stream()
            .map(Material::getType)
            .distinct()
            .count();
        
        int mediaCount = (int) inventory.stream()
            .filter(m -> m instanceof Media)
            .count();
        
        int printCount = (int) inventory.stream()
            .filter(m -> m instanceof PrintedBook || m instanceof Magazine)
            .count();
        
        return new InventoryStats(
            inventory.size(),
            averagePrice,
            medianPrice,
            uniqueTypes,
            mediaCount,
            printCount
        );
    }
    
    @Override
    public synchronized void clearInventory() {
        inventory.clear();
        idIndex.clear();
    }
    
    @Override
    public int size() {
        return inventory.size();
    }
    
    @Override
    public boolean isEmpty() {
        return inventory.isEmpty();
    }
    
    @Override
    public List<Material> findRecentMaterials(int years) {
        if (years < 0) {
            throw new IllegalArgumentException("Years cannot be negative: " + years);
        }
        
        int currentYear = java.time.Year.now().getValue();
        int cutoffYear = currentYear - years;
        
        return inventory.stream()
            .filter(material -> material.getYear() >= cutoffYear)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> findByCreators(String... creators) {
        if (creators == null || creators.length == 0) {
            return new ArrayList<>();
        }
        
        Set<String> creatorSet = Arrays.stream(creators)
            .filter(Objects::nonNull)
            .map(String::trim)
            .filter(s -> !s.isEmpty())
            .collect(Collectors.toSet());
        
        if (creatorSet.isEmpty()) {
            return new ArrayList<>();
        }
        
        return inventory.stream()
            .filter(material -> creatorSet.contains(material.getCreator()))
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> findWithPredicate(Predicate<Material> condition) {
        if (condition == null) {
            throw new NullPointerException("Predicate cannot be null");
        }
        
        return inventory.stream()
            .filter(condition)
            .collect(Collectors.toList());
    }
    
    @Override
    public List<Material> getSorted(Comparator<Material> comparator) {
        if (comparator == null) {
            throw new NullPointerException("Comparator cannot be null");
        }
        
        return inventory.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    }
    
    /**
     * Demonstrates polymorphic behavior by getting display info for all materials.
     * 
     * @return list of display strings
     */
    public List<String> getAllDisplayInfo() {
        return inventory.stream()
            .map(Material::getDisplayInfo)
            .collect(Collectors.toList());
    }
    
    /**
     * Groups materials by type for reporting.
     * 
     * @return map of type to materials
     */
    public Map<Material.MaterialType, List<Material>> groupByType() {
        return inventory.stream()
            .collect(Collectors.groupingBy(Material::getType));
    }
    
    /**
     * Gets materials with active discounts.
     * 
     * @return list of discounted materials
     */
    public List<Material> getDiscountedMaterials() {
        return inventory.stream()
            .filter(m -> m.getDiscountRate() > 0)
            .collect(Collectors.toList());
    }
    
    /**
     * Calculates total savings from discounts.
     * 
     * @return total discount amount
     */
    public double getTotalDiscountAmount() {
        return inventory.stream()
            .mapToDouble(m -> m.getPrice() * m.getDiscountRate())
            .sum();
    }
    
    @Override
    public String toString() {
        return String.format("MaterialStore[Size=%d, Types=%d, Value=$%.2f]",
            size(),
            groupByType().size(),
            getTotalInventoryValue());
    }
}