import {throwNullException} from './NullException';
import {StringBuilder} from './StringBuilder';
import {ListDefinition} from './ListDefinition';
import {Story} from './Story';

export class InkListItem implements IInkListItem{ // InkListItem is a struct

	public readonly originName: string | null = null;
	public readonly itemName: string | null = null;

	constructor(originName: string | null, itemName: string | null)
	constructor(fullName: string | null)
	constructor(){
		if (typeof arguments[1] !== 'undefined'){
			let originName = arguments[0] as string | null;
			let itemName = arguments[1] as string | null;

			this.originName = originName;
			this.itemName = itemName;
		}
		else if (arguments[0]){
			let fullName = arguments[0] as string;

			let nameParts = fullName.toString().split('.');
			this.originName = nameParts[0];
			this.itemName = nameParts[1];
		}
	}
	public static get Null(){
		return new InkListItem(null, null);
	}
	public get isNull(){
		return this.originName == null && this.itemName == null;
	}
	get fullName(){
		return ((this.originName !== null) ? this.originName : '?') + '.' + this.itemName;
	}
	public toString(): string {
		return this.fullName;
	}
	public Equals(obj: InkListItem){
		if (obj instanceof InkListItem) {
			let otherItem = obj;
			return otherItem.itemName == this.itemName
				&& otherItem.originName == this.originName;
		}

		return false;
	}

	// These methods did not exist in the original C# code. Their purpose is to
	// make `InkListItem` mimics the value-type semantics of the original
	// struct. Please refer to the end of this file, for a more in-depth
	// explanation.

	/**
	 * Returns a shallow clone of the current instance.
	 */
	public copy(){
		return new InkListItem(this.originName, this.itemName);
	}
	/**
	 * Returns a `SerializedInkListItem` representing the current
	 * instance. The result is intended to be used as a key inside a Map.
	 */
	public serialized(): SerializedInkListItem{
		// We are simply using a JSON representation as a value-typed key.
		return JSON.stringify({originName: this.originName, itemName: this.itemName});
	}

	/**
	 * Reconstructs a `InkListItem` from the given SerializedInkListItem.
	 */
	public static fromSerializedKey(key: SerializedInkListItem): InkListItem {
		let obj = JSON.parse(key);
		if (!InkListItem.isLikeInkListItem(obj)) return InkListItem.Null;

		let inkListItem = obj as IInkListItem;

		return new InkListItem(inkListItem.originName, inkListItem.itemName);
	}

	/**
	 * Determines whether the given item is sufficiently `InkListItem`-like
	 * to be used as a template when reconstructing the InkListItem.
	 */
	private static isLikeInkListItem(item: any){
		if (typeof item !== 'object') return false;
		if (!item.hasOwnProperty('originName') || !item.hasOwnProperty('itemName')) return false;
		if (typeof item.originName !== 'string' && typeof item.originName !== null) return false;
		if (typeof item.itemName !== 'string' && typeof item.itemName !== null) return false;

		return true;
	}
}

export class InkList extends Map<SerializedInkListItem, number> {
	public origins: ListDefinition[] | null = null;
	public _originNames: string[] | null = [];

	constructor()
	constructor(otherList: InkList)
	constructor(singleOriginListName: string, originStory: Story)
	constructor(singleElement: KeyValuePair<InkListItem, number>)
	constructor(){
		// Trying to be smart here, this emulates the constructor inheritance found
		// in the original code, but only if otherList is an InkList. IIFE FTW.
		// @ts-ignore
		super((() => {
			if (arguments[0] instanceof InkList){
				return arguments[0];
			}
			else{
				return undefined;
			}
		})());

		if (arguments[0] instanceof InkList){
			let otherList = arguments[0] as InkList;

			if (otherList._originNames) {
				this._originNames = otherList._originNames.slice();
			}
		}
		else if (typeof arguments[0] === 'string'){
			let singleOriginListName = arguments[0] as string;
			let originStory = arguments[1] /* as Story */;
			this.SetInitialOriginName(singleOriginListName);

			let def = originStory.listDefinitions.TryListGetDefinition(singleOriginListName, null);
			if (def.exists){
				this.origins = [def.result];
			}
			else{
				throw new Error('InkList origin could not be found in story when constructing new list: ' + singleOriginListName);
			}
		}
		else if (typeof arguments[0] === 'object' && arguments[0].hasOwnProperty('Key') && arguments[0].hasOwnProperty('Value')){
			let singleElement = arguments[0] as KeyValuePair<InkListItem, number>;
			this.Add(singleElement.Key, singleElement.Value);
		}
	}

	// @ts-ignore
	public AddItem(itemOrItemName: InkListItem | string | null){
		if (itemOrItemName instanceof InkListItem){
			let item = itemOrItemName;

			if (item.originName == null) {
				this.AddItem(item.itemName);
				return;
			}

			if (this.origins === null) return throwNullException('this.origins');

			for (let origin of this.origins) {
				if (origin.name == item.originName) {
					let intVal = origin.TryGetValueForItem(item, 0);
					if (intVal.exists) {
						this.Add(item, intVal.result);
						return;
					} else {
						throw new Error('Could not add the item ' + item + " to this list because it doesn't exist in the original list definition in ink.");
					}
				}
			}

			throw new Error("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found.");
		}
		else {
			let itemName = itemOrItemName as string | null;

			let foundListDef: ListDefinition | null = null;

			if (this.origins === null) return throwNullException('this.origins');

			for (let origin of this.origins) {
				if (itemName === null) return throwNullException('itemName');

				if (origin.ContainsItemWithName(itemName)) {
						if (foundListDef != null) {
							throw new Error('Could not add the item ' + itemName + ' to this list because it could come from either ' + origin.name + ' or ' + foundListDef.name);
						} else {
							foundListDef = origin;
						}
				}
			}

			if (foundListDef == null)
				throw new Error('Could not add the item ' + itemName + " to this list because it isn't known to any list definitions previously associated with this list.");

			let item = new InkListItem(foundListDef.name, itemName);
			let itemVal = foundListDef.ValueForItem(item);
			this.Add(item, itemVal);
		}
	}
	public ContainsItemNamed(itemName: string | null){
		for (let [key] of this) {
			let item = InkListItem.fromSerializedKey(key);
			if (item.itemName == itemName) return true;
		}

		return false;
	}
	public ContainsKey(key: InkListItem){
		return this.has(key.serialized());
	}
	public Add(key: InkListItem, value: number){
		let serializedKey = key.serialized();
		if (this.has(serializedKey)) {
			// Throw an exception to match the C# behavior.
			throw new Error(`The Map already contains an entry for ${key}`);
		}
		this.set(serializedKey, value);
	}
	public Remove(key: InkListItem){
		return this.delete(key.serialized());
	}
	get Count(){
		return this.size;
	}
	get originOfMaxItem(): ListDefinition | null{
		if (this.origins == null) return null;

		let maxOriginName = this.maxItem.Key.originName;
		let result = null;
		this.origins.every((origin)=>{
			if (origin.name == maxOriginName){
				result = origin;
				return false;
			}
			else return true;
		});

		return result;
	}
	get originNames(): string[]{
		if (this.Count > 0) {
			if (this._originNames == null && this.Count > 0)
				this._originNames = [];
			else {
				if (!this._originNames) this._originNames = [];
				this._originNames.length = 0;
			}

			for (let [key] of this) {
				let item = InkListItem.fromSerializedKey(key);
				if (item.originName === null) return throwNullException('item.originName');
				this._originNames.push(item.originName);
			}
		}

		return this._originNames as string[];
	}
	public SetInitialOriginName(initialOriginName: string){
		this._originNames = [initialOriginName];
	}
	public SetInitialOriginNames(initialOriginNames: string[]){
		if (initialOriginNames == null)
			this._originNames = null;
		else
			this._originNames = initialOriginNames.slice();// store a copy
	}
	get maxItem(){
		let max: KeyValuePair<InkListItem, number> = {
			Key: InkListItem.Null,
			Value: 0,
		};
		for (let [key, value] of this) {
			let item = InkListItem.fromSerializedKey(key);
			if (max.Key.isNull || value > max.Value)
				max = { Key: item, Value: value };
		}

		return max;
	}
	get minItem(){
		let min: KeyValuePair<InkListItem, number> = {
			Key: InkListItem.Null,
			Value: 0,
		};
		for (let [key, value] of this) {
			let item = InkListItem.fromSerializedKey(key);
			if (min.Key.isNull || value < min.Value) {
				min = { Key: item, Value: value };
			}
		}
		return min;
	}
	get inverse(){
		let list = new InkList();
		if (this.origins != null) {
			for (let origin of this.origins) {
				for (let [key, value] of origin.items) {
					let item = InkListItem.fromSerializedKey(key);
					if (!this.ContainsKey(item))
						list.Add(item, value);
				}
			}
		}
		return list;
	}
	get all(){
		let list = new InkList();
		if (this.origins != null) {
			for(let origin of this.origins) {
				for (let [key, value] of origin.items) {
					let item = InkListItem.fromSerializedKey(key);
					list.set(item.serialized(), value);
				}
			}
		}
		return list;
	}
	public Union(otherList: InkList){
		let union = new InkList(this);
		for(let [key, value] of otherList) {
			union.set(key, value);
		}
		return union;
	}
	public Intersect(otherList: InkList){
		let intersection = new InkList();
		for(let [key, value] of this) {
			if (otherList.has(key))
				intersection.set(key, value);
		}

		return intersection;
	}
	public Without(listToRemove: InkList){
		let result = new InkList(this);
		for(let [key] of listToRemove) {
			result.delete(key);
		}

		return result;
	}
	public Contains(otherList: InkList){
		for(let [key] of otherList) {
			if (!this.has(key)) return false;
		}

		return true;
	}
	public GreaterThan(otherList: InkList){
		if (this.Count == 0) return false;
		if (otherList.Count == 0) return true;

		return this.minItem.Value > otherList.maxItem.Value;
	}
	public GreaterThanOrEquals(otherList: InkList){
		if (this.Count == 0) return false;
		if (otherList.Count == 0) return true;

		return this.minItem.Value >= otherList.minItem.Value
			&& this.maxItem.Value >= otherList.maxItem.Value;
	}
	public LessThan(otherList: InkList){
		if (otherList.Count == 0) return false;
		if (this.Count == 0) return true;

		return this.maxItem.Value < otherList.minItem.Value;
	}
	public LessThanOrEquals(otherList: InkList){
		if (otherList.Count == 0) return false;
		if (this.Count == 0) return true;

		return this.maxItem.Value <= otherList.maxItem.Value
			&& this.minItem.Value <= otherList.minItem.Value;
	}
	public MaxAsList(){
		if (this.Count > 0)
			return new InkList(this.maxItem);
		else
			return new InkList();
	}
	public MinAsList(){
		if (this.Count > 0)
			return new InkList(this.minItem);
		else
			return new InkList();
	}
	public ListWithSubRange(minBound: any, maxBound: any)
	{
		if (this.Count == 0) return new InkList();

		let ordered = this.orderedItems;

		let minValue = 0;
		let maxValue = Number.MAX_SAFE_INTEGER;

		if (Number.isInteger(minBound)) {
			minValue = minBound;
		} else {
			if (minBound instanceof InkList && minBound.Count > 0 )
				minValue = minBound.minItem.Value;
		}

		if (Number.isInteger(maxBound)) {
			maxValue = maxBound;
		} else {
			if (minBound instanceof InkList && (minBound).Count > 0)
				maxValue = maxBound.maxItem.Value;
		}

		let subList = new InkList();
		subList.SetInitialOriginNames(this.originNames);
		for (let item of ordered) {
			if (item.Value >= minValue && item.Value <= maxValue ) {
				subList.Add(item.Key, item.Value);
			}
		}

		return subList;
	}
	public Equals(otherInkList: InkList){
		if (otherInkList instanceof InkList === false) return false;
		if (otherInkList.Count != this.Count) return false;

		for(let [key] of this) {
			if (!otherInkList.has(key))
				return false;
		}

		return true;
	}
	// GetHashCode not implemented
	get orderedItems() {
		// List<KeyValuePair<InkListItem, int>>
		let ordered = new Array<KeyValuePair<InkListItem, number>>();

		for(let [key, value] of this) {
			let item = InkListItem.fromSerializedKey(key);
			ordered.push({ Key: item, Value: value });
		}

		ordered.sort((x, y) => {
			if (x.Key.originName === null) { return throwNullException('x.Key.originName'); }
			if (y.Key.originName === null) { return throwNullException('y.Key.originName'); }

			if (x.Value == y.Value) {
				return x.Key.originName.localeCompare(y.Key.originName);
			} else {
				// TODO: refactor this bit into a numberCompareTo method?
				if (x.Value < y.Value)
					return -1;
				return x.Value > y.Value ? 1 : 0;
			}
		});

		return ordered;
	}
	public toString(){
		let ordered = this.orderedItems;

		let sb = new StringBuilder();
		for (let i = 0; i < ordered.length; i++) {
			if (i > 0)
				sb.Append(', ');

			let item = ordered[i].Key;
			if (item.itemName === null) return throwNullException('item.itemName');
			sb.Append(item.itemName);
		}

		return sb.toString();
	}
	// casting a InkList to a Number, for somereason, actually gives a number.
	// This messes up the type detection when creating a Value from a InkList.
	// Returning NaN here prevents that.
	public valueOf(){
		return NaN;
	}
}

/**
 * In the original C# code, `InkListItem` was defined as value type, meaning
 * that two `InkListItem` would be considered equal as long as they held the
 * same values. This doesn't hold true in Javascript, as `InkListItem` is a
 * reference type (Javascript doesn't allow the creation of custom value types).
 *
 * The key equality of Map objects is based on the "SameValueZero" algorithm;
 * since `InkListItem` is a value type, two keys will only be considered
 * equal if they are, in fact, the same object. As we are trying to emulate
 * the original behavior as close as possible, this will lead to unforeseen
 * side effects.
 *
 * In order to have a key equality based on value semantics, we'll convert
 * `InkListItem` to a valid string representation and use this representation
 * as a key (strings are value types in Javascript). Rather than using the
 * type `string` directly, we'll alias it to `SerializedInkListItem` and use
 * this type as the key for our Map-based `InkList`.
 *
 * Reducing `InkListItem` to a JSON representation would not be bulletproof
 * in the general case, but for our needs it works well. The major downside of
 * this method is that we will have to to reconstruct the original `InkListItem`
 * every time we'll need to access its properties.
 */
export type SerializedInkListItem = string;

/**
 * An interface inherited by `InkListItem`, defining exposed
 * properties. It's mainly used when deserializing a `InkListItem` from its
 * key (`SerializedInkListItem`)
 */
interface IInkListItem{
	readonly originName: string | null;
	readonly itemName: string | null;
}
export interface KeyValuePair<K, V> {
	Key: K;
	Value: V;
}
