package com.packtpub.hibernatesearch.domain;

import java.io.Serializable;
import java.util.Date;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import org.apache.solr.analysis.HTMLStripCharFilterFactory;
import org.apache.solr.analysis.PhoneticFilterFactory;
import org.apache.solr.analysis.SnowballPorterFilterFactory;
import org.apache.solr.analysis.StandardFilterFactory;
import org.apache.solr.analysis.StandardTokenizerFactory;
import org.apache.solr.analysis.StopFilterFactory;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Analyzer;
import org.hibernate.search.annotations.AnalyzerDef;
import org.hibernate.search.annotations.CharFilterDef;
import org.hibernate.search.annotations.Boost;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Fields;
import org.hibernate.search.annotations.FullTextFilterDefs;
import org.hibernate.search.annotations.FullTextFilterDef;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.IndexedEmbedded;
import org.hibernate.search.annotations.NumericField;
import org.hibernate.search.annotations.Parameter;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
import org.hibernate.search.annotations.TokenFilterDef;
import org.hibernate.search.annotations.TokenizerDef;

import com.packtpub.hibernatesearch.util.DeviceFilterFactory;
import com.packtpub.hibernatesearch.util.IndexWhenActiveInterceptor;

/**
 * Prosta klasa encji reprezentująca aplikację komputerową ... z nazwą, długim opisem 
 * i nazwą pliku użytego jako ikona. Klasa posiada flagę "active" określającą, czy dana aplikacja
 * ma być wyszukiwalna.  
 * 
 * Adnotacja @Entity nakazuje Hibernate by zmapowało klasę do tabeli w bazie danych, podczas gdy adnotacja @Indexed 
 * nakazuje narzędziu Hibernate Search by zmapowało ją do indeksu Lucene. Element "interceptor" ustawia warunki 
 * indeksowania by zapobiec indeksowaniu instancji zawierających flagi "active" ustawione na "false" (patrz klasa 
 * "com.packtpub.hibernatesearch.util.IndexWhenActiveInterceptor").
 * 
 * Adnotacja @AnalyzerDef definiuje powiązanie analyzera z polem "description". Analyzer usuwa tagi HTML oraz
 * dodaje kilka filtrów tokenów usuwających literówki.
 */
@Entity
@Indexed(interceptor=IndexWhenActiveInterceptor.class)
@AnalyzerDef(
	name="appAnalyzer",
	charFilters={ @CharFilterDef(factory=HTMLStripCharFilterFactory.class) },
	tokenizer=@TokenizerDef(factory=StandardTokenizerFactory.class),
	filters={ 
		@TokenFilterDef(factory=StandardFilterFactory.class),
		@TokenFilterDef(factory=StopFilterFactory.class),
		@TokenFilterDef(factory=PhoneticFilterFactory.class, params = {
        	@Parameter(name="encoder", value="DoubleMetaphone")
		}),
		@TokenFilterDef(factory=SnowballPorterFilterFactory.class, params = {
			@Parameter(name="language", value="English") 
		})
	}
)
@Boost(2.0f)
@FullTextFilterDefs({
	@FullTextFilterDef(name="deviceName", impl=DeviceFilterFactory.class)
})
public class App implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * Klucz główny, skonfigurowany tak, by był generowany automatycznie podczas tworzenia nowej instancji
	 */
	@Id
	@GeneratedValue
	private Long id;

	/**
	 * Nazwa aplikacji zrozumiała dla ludzi. Adnotacja @Column informuje Hibernate by zmapował to pole do 
	 * kolumny w bazie danych.
	 * 
	 * Obecnie używamy dwóch adnotacji @Field w celu dwukrotnego indeksowania... Pierwsze z przetworzonymi (rozbitymi) 
	 * wartościami dla łatwiejszego wyszukiwania, drugie nieprzetworzone (nie rozbite) aby na podstawie ich wartości 
	 * sortować wyniki.
	 * 
	 * Adnotacja @Boost użyta na poziomie pola nadaje mu jeszcze większą wagę podczas wyliczania ważności wyników wyszukiwania.
	 * Wartość 1.5 kumuluje się z wartością 2.0 ustawioną na klasie.
	 */
	@Column
	@Fields({
		@Field(store=Store.COMPRESS),
		@Field(name="sorting_name", analyze=Analyze.NO)
	})
	@Boost(1.5f)
	private String name;

	/**
	 * Dłuższy, bardziej szczegółowy opis aplikacji. Adnotacja @Column informuje Hibernate by zmapował to pole do 
	 * kolumny w bazie danych, natomiast adnotacja @Field nakazuje Hibernate Search by zmapował je jako pole  
	 * indeksie Lucene.
	 * 
	 * Adnotacja @Analyzer wiąże pole ze zdefiniowanym powyżej analyzerem.
	 * 
	 * Adnotacja @Boost użyta na poziomie pola nadaje mu jeszcze większą wagę podczas wyliczania ważności wyników wyszukiwania. 
	 * Wartość 1.2 kumuluje się z wartością 2.0 ustawioną na klasie.
	 */
	@Column(length = 1000)
	@Field(store=Store.COMPRESS)
	@Analyzer(definition="appAnalyzer")
	@Boost(1.2f)
	private String description;
	
	@Column
	@Field
	private String category;

	/**
	 * Nazwa pliku obrazka mającego być powiązanym z aplikacją. Spodziewamy się, że plik będzie istniał w podkatalogu 
	 * "images/apps/<obrazek>".  To pole nie ma adnotacji @Field, ponieważ nie planujemy przeszukiwania aplikacji pod  
	 * kątem nazw plików obrazków.
	 */
	@Column
	@Field(store=Store.COMPRESS)
	private String image;
	
	/**
	 * Cena aplikacji. Adnotacja @NumericField instruuje Hibernate Search by indeksował pole używając
	 * wyspecjalizowanej struktury danych, by usprawnić sortowanie oraz zapytania oparte o zakres.
	 */
	@Column
	@Field
	@NumericField
	private float price;
	
	/**
	 * Flaga określająca, czy aplikacja ma być wyszukiwalna.
	 */
	@Column
	private boolean active;

	/**
	 * Data wydania aplikacji.  Opcjonalna adnotacja @DateBridge używa elementu "resolution" by zadeklarować,
	 * że data ma być indeksowana do poziomu dnia... zamiast standardowego podejścia tj. zapisywania 
	 * pełnej daty, co do milisekundy.
	 */
	@Column
	@Field
	@DateBridge(resolution=Resolution.DAY)
	private Date releaseDate;

	/**
	 * Kolekcja powiązanych encji typu Device, reprezentujących urządzenia na których można uruchomić aplikację.
	 * 
	 * Adnotacja @ManyToMany informuje Hibernate, że aplikacja może być uruchamiana na wielu urządzeniach, oraz że
	 * jedno urządzenie może uruchamiać wiele aplikacji  
	 * 
	 *      Element "fetch" ma ustawioną wartość "eager" (zamiast standardowej "lazy") aby wszystkie interesujące nas obiekty typu 
	 *      Device zostały pobrane jednocześnie dopóki sesja Hibernate jest otwarta. W innym przypadku pojawiłyby się błędy gdyby 
	 *      warstwa widoku spróbowała dostać się do tych pól po zamknięciu sesji Hibernate. Zazwyczaj chciwe pobieranie jest mniej  
	 *      wydajne od leniwego. W większej aplikacji powinieneś się zastanowić nad innym podejściem, np. użyciem wzorca DAO. 
	 * 
	 *      Element "cascade" zapewnia, że zmiany po dowolnej stronie relacji App-Device będą poprawnie odzwierciedlone w indeksach 
	 *      Lucene obu encji.
	 * 
	 * Adnotacja @IndexedEmbedded informuje Hibernate Search by indeksował powiązane encje.  
	 * Klasa Device może (ale nie musi) posiadać indeks Lucene (w zależności od tego czy klasa jest adnotowana @Indexed), 
	 * ale i tak instancje Device powiązane z aplikacją będą przechowywane w indeksie Lucene klasy App.
	 */
	@ManyToMany(fetch=FetchType.EAGER, cascade = { CascadeType.ALL })
	@IndexedEmbedded(depth=1)
	private Set<Device> supportedDevices;
	
	/**
	 * Kolekcja wbudowanych obiektów prezentujących opinie użytkowników o aplikacji. Wbudowane obiekty różnią się od powiązanych encji 
	 * tym, że z chwilą skasowania obiektu, z którym są powiązane one również zostają usunięte. 
	 * 
	 * Adnotacja @ElementCollection informuje Hibernate o powiązaniu z wieloma obiektami wbudowanymi, w podobny sposób co adnotacja 
	 * @ManyToMany użyta dla "supportedDevices".  Element "fetch" ma ustawioną wartość "Eager" z tego samego powodu co w przypadku 
	 * "SupportedDevices".   
	 *
	 * Adnotacja @Fetch instruuje Hibernate by pobrało wbudowane obiekty używając wielu poleceń SELECT zamiast jednego rozbudowanego 
	 * JOIN (domyślnego zachowania).  Takie podejście jest niewydajne dla dużych zbiorów danych ... ale spodziewamy się, że pojedyńcza 
	 * aplikacja nie będzie miała zbyt wielu komentarzy. Dzięki temu podejściu unikniemy zwrócenia przez Hibernate zduplikowanych 
	 * komentarzy (http://stackoverflow.com/questions/1093153/hibernate-collectionofelements-eager-fetch-duplicates-elements). Jak  
	 * wspomniano w komentarzu do "supportedDevices", w większych aplikacjach zastanów się nad refaktoringiem rozwiązania tak, aby 
	 * uniknąć zachłannego pobierania obiektów.
	 */
	@ElementCollection(fetch=FetchType.EAGER)
	@Fetch(FetchMode.SELECT)
	@IndexedEmbedded(depth=1, includePaths = { "stars", "comments" })
	private Set<CustomerReview> customerReviews;

	/**
	 * Domyślny pusty konstruktor.
	 */
	public App() {
	}

	
	/**
	 * Wygodny konstruktor, ustawiający zawartość wszystkich pól w jednym kroku.  
	 * 
	 * Parametr "active" jest domyślnie ustawiony na wartość "true".  Aby dezaktywować aplikację,
	 * użyj settera dla tego pola. 
	 */
	public App(String name, String image, String description, String category, float price) {
		this.name = name;
		this.image = image;
		this.description = description;
		this.category = category;
		this.price = price;
		
		// Pozostałe pola inicjujemy z sensownymi wartościami domyślnymi. StartupDataLoader może je 
		// zmienić setterami
		this.active = true;
		this.releaseDate = new Date();
	}

	//
	// GETTERY I SETTERY
	//
	
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getCategory() {
		return category;
	}

	public void setCategory(String category) {
		this.category = category;
	}

	public String getImage() {
		return image;
	}

	public void setImage(String image) {
		this.image = image;
	}

	public Set<Device> getSupportedDevices() {
		return supportedDevices;
	}

	public void setSupportedDevices(Set<Device> supportedDevices) {
		this.supportedDevices = supportedDevices;
	}

	public boolean isActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

	public Set<CustomerReview> getCustomerReviews() {
		return customerReviews;
	}

	public void setCustomerReviews(Set<CustomerReview> customerReviews) {
		this.customerReviews = customerReviews;
	}

	public Date getReleaseDate() {
		return releaseDate;
	}

	public void setReleaseDate(Date releaseDate) {
		this.releaseDate = releaseDate;
	}

	public float getPrice() {
		return price;
	}

	public void setPrice(float price) {
		this.price = price;
	}

}
