// Import the core angular services.
import { combineLatest } from "rxjs";
import { Injectable } from "@angular/core";
import { map } from "rxjs/operators";
import { Observable } from "rxjs";
 
// Import the application components and services.
import { AppStorageService } from "./app-storage.service";
import { SimpleStore } from "./simple.store";
 
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
 
export interface SantaState {
	v: number;
	selectedListType: ListType;
	nicePeople: Person[];
	naughtyPeople: Person[];
}
 
export interface Person {
	id: number;
	name: string;
}
 
export type ListType = "nice" | "naughty";
 
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
 
@Injectable({
	providedIn: "root"
})
export class SantaRuntime {
 
	private appStorage: AppStorageService;
	private appStorageKey: string;
	private store: SimpleStore<SantaState>;
 
	// I initialize the Santa runtime.
	constructor( appStorage: AppStorageService ) {
 
		this.appStorage = appStorage;
		this.appStorageKey = "santa_runtime_storage";
 
		// NOTE: For the store instance we are NOT USING DEPENDENCY-INJECTION. That's
		// because the store isn't really a "behavior" that we would ever want to swap -
		// it's just a slightly more complex data structure. In reality, it's just a
		// fancy hash/object that can also emit values.
		this.store = new SimpleStore( this.getInitialState() );
 
		// Because this runtime wants to persist data across page reloads, let's register
		// an unload callback so that we have a chance to save the data as the app is
		// being unloaded.
		this.appStorage.registerUnloadCallback( this.saveToStorage );
 
	}
 
	// ---
	// PUBLIC METHODS.
	// ---
 
	// I add the given person to the currently-selected list.
	public async addPerson( name: string ) : Promise<number> {
 
		var person = {
			id: Date.now(),
			name: name
		};
 
		var state = this.store.getSnapshot();
 
		if ( state.selectedListType === "nice" ) {
 
			this.store.setState({
				nicePeople: state.nicePeople.concat( person )
			});
 
		} else {
 
			this.store.setState({
				naughtyPeople: state.naughtyPeople.concat( person )
			});
 
		}
 
		return( person.id );
 
	}
 
 
	// I return a stream that contains the number of people on the naughty list.
	public getNaughtyCount() : Observable<number> {
 
		return( this.getListCount( "naughtyPeople" ) );
 
	}
 
 
	// I return a stream that contains the number of people on the nice list.
	public getNiceCount() : Observable<number> {
 
		return( this.getListCount( "nicePeople" ) );
 
	}
 
 
	// I return a stream that contains the people in the currently-selected list.
	public getPeople() : Observable<Person[]> {
 
		var stream = combineLatest(
			this.store.select( "selectedListType" ),
			this.store.select( "nicePeople" ),
			this.store.select( "naughtyPeople" )
		);
 
		var reducedStream = stream.pipe(
			map(
				([ selectedListType, nicePeople, naughtyPeople ]) => {
 
					if ( selectedListType === "nice" ) {
 
						return( nicePeople );
 
					} else {
 
						return( naughtyPeople );
 
					}
 
				}
			)
		);
 
		return( reducedStream );
 
	}
 
 
	// I return a stream that contains the currently selected list type.
	public getSelectedListType() : Observable<ListType> {
 
		return( this.store.select( "selectedListType" ) );
 
	}
 
 
	// I remove the person with given ID from the naughty and nice lists.
	public async removePerson( id: number ) : Promise<void> {
 
		var state = this.store.getSnapshot();
		var nicePeople = state.nicePeople;
		var naughtyPeople = state.naughtyPeople;
 
		// Keep the people that don't have a matching ID.
		var filterInWithoutId = ( person: Person ) : boolean => {
 
			return( person.id !== id );
 
		};
 
		this.store.setState({
			nicePeople: nicePeople.filter( filterInWithoutId ),
			naughtyPeople: naughtyPeople.filter( filterInWithoutId )
		});
 
	}
 
 
	// I select the given list.
	public async selectList( listType: ListType ) : Promise<void> {
 
		this.store.setState({
			selectedListType: listType
		});
 
	}
 
	// ---
	// PRIVATE METHODS.
	// ---
 
	// I return the initial state for the underlying store.
	private getInitialState() : SantaState {
 
		// NOTE: Because we are using a string-literal as a "type", we have to help
		// TypeScript by using a type annotation on our initial state. Otherwise, it
		// won't be able to infer that our string is compatible with the type.
		var initialState: SantaState = {
			v: 3,
			selectedListType: "nice",
			nicePeople: [
				{
					id: 1,
					name: "Jon"
				}
			],
			naughtyPeople: [
				{
					id: 2,
					name: "Seema"
				}
			]
		};
 
		// See if we have any persisted store (returns NULL if not available).
		// --
		// CAUTION: The parent function is being called in a way that is expecting the
		// execution to by SYNCHRONOUS, which localStorage is. If the AppStorageService
		// were to return a Promise<data>, it would be less "blocking"; but, it would
		// also require us to rework the way we are initialing the store.
		var savedState = this.appStorage.loadData<SantaState>( this.appStorageKey );
 
		// If we have saved data AND the data structure is the same VERSION as the one
		// we expect, then return it as the initial state.
		if ( savedState && ( savedState.v === initialState.v ) ) {
 
			return( savedState );
 
		} else {
 
			return( initialState );
 
		}
 
	}
 
 
	// I return a stream that contains the count for the given Person collection.
	private getListCount( list: "nicePeople" | "naughtyPeople" ) : Observable<number> {
 
		var stream = this.store.select( list );
 
		var reducedStream = stream.pipe(
			map(
				( people ) => {
 
					return( people.length );
 
				}
			)
		);
 
		return( reducedStream );
 
	}
 
 
	// I save the current state to given store object.
	// --
	// CAUTION: Using a fat-arrow function to bind callback to instance.
	private saveToStorage = () : void => {
 
		this.appStorage.saveData( this.appStorageKey, this.store.getSnapshot() );
 
	}
 
}