import {InkObject} from './Object';
import {Path} from './Path';
import {InkList, InkListItem} from './InkList';
import {StoryException} from './StoryException';
import {asOrNull, asOrThrows} from './TypeAssertion';
import {tryParseInt, tryParseFloat} from './TryGetResult';
import {throwNullException} from './NullException';

export abstract class AbstractValue extends InkObject{
	public abstract get valueType(): ValueType;
	public abstract get isTruthy(): boolean;
	public abstract get valueObject(): any;

	public abstract Cast(newType: ValueType): Value<any>;

	public static Create(val: any): Value<any> | null{
		// Implicitly convert bools into ints
		if (typeof val === 'boolean'){
			let b = !!val;
			val = (b) ? 1 : 0;
		}

		if (Number.isInteger(Number(val))) {
			return new IntValue(Number(val));
		} else if (!isNaN(val)) {
			return new FloatValue(Number(val));
		} else if (typeof val === 'string') {
			return new StringValue(String(val));
		} else if (val instanceof Path) {
			return new DivertTargetValue(asOrThrows(val, Path));
		} else if (val instanceof InkList) {
			return new ListValue(asOrThrows(val, InkList));
		}

		return null;
	}
	public Copy() {
		return asOrThrows(AbstractValue.Create(this), InkObject);
	}
	public BadCastException(targetType: ValueType) {
		return new StoryException("Can't cast "+this.valueObject+' from ' + this.valueType+' to '+targetType);
	}
}

export abstract class Value<T extends { toString: () => string; }> extends AbstractValue{
	public value: T | null;

	constructor(val: T | null){
		super();
		this.value = val;
	}
	public get valueObject(){
		return this.value;
	}
	public toString(){
		if (this.value === null) return throwNullException('Value.value');
		return this.value.toString();
	}
}

export class IntValue extends Value<number>{
	constructor(val: number){
		super(val || 0);
	}
	public get isTruthy(){
		return this.value != 0;
	}
	public get valueType() {
		return ValueType.Int;
	}

	public Cast(newType: ValueType): Value<any>{
		if (this.value === null) return throwNullException('Value.value');

		if (newType == this.valueType) {
			return this;
		}

		if (newType == ValueType.Float) {
			return new FloatValue(this.value);
		}

		if (newType == ValueType.String) {
			return new StringValue('' + this.value);
		}

		throw this.BadCastException(newType);
	}
}

export class FloatValue extends Value<number>{
	constructor(val: number){
		super(val || 0.0);
	}
	public get isTruthy(){
		return this.value != 0.0;
	}
	public get valueType(){
		return ValueType.Float;
	}

	public Cast(newType: ValueType): Value<any>{
		if (this.value === null) return throwNullException('Value.value');

		if (newType == this.valueType) {
			return this;
		}

		if (newType == ValueType.Int) {
			return new IntValue(this.value);
		}

		if (newType == ValueType.String) {
			return new StringValue('' + this.value);
		}

		throw this.BadCastException(newType);
	}
}

export class StringValue extends Value<string>{
	public _isNewline: boolean;
	public _isInlineWhitespace: boolean;

	constructor(val: string){
		super(val || '');

		this._isNewline = (this.value == '\n');
		this._isInlineWhitespace = true;

		if (this.value === null) return throwNullException('Value.value');

		if (this.value.length > 0) {
			this.value.split('').every((c) => {
				if (c != ' ' && c != '\t'){
					this._isInlineWhitespace = false;
					return false;
				}

				return true;
			});
		}
	}
	public get valueType(){
		return ValueType.String;
	}
	public get isTruthy(){
		if (this.value === null) return throwNullException('Value.value');
		return this.value.length > 0;
	}
	public get isNewline(){
		return this._isNewline;
	}
	public get isInlineWhitespace(){
		return this._isInlineWhitespace;
	}
	public get isNonWhitespace(){
		return !this.isNewline && !this.isInlineWhitespace;
	}

	public Cast(newType: ValueType): Value<any>{
		if (newType == this.valueType) {
			return this;
		}

		if (newType == ValueType.Int) {

			let parsedInt = tryParseInt(this.value);
			if (parsedInt.exists) {
				return new IntValue(parsedInt.result);
			} else {
				throw this.BadCastException(newType);
			}
		}

		if (newType == ValueType.Float) {
			let parsedFloat = tryParseFloat(this.value);
			if (parsedFloat.exists) {
				return new FloatValue(parsedFloat.result);
			} else {
				throw this.BadCastException(newType);
			}
		}

		throw this.BadCastException(newType);
	}
}

export class DivertTargetValue extends Value<Path>{
	constructor(targetPath: Path){
		super(targetPath);
	}
	public get valueType() {
		return ValueType.DivertTarget;
	}
	public get targetPath(){
		if (this.value === null) return throwNullException('Value.value');
		return this.value;
	}
	public set targetPath(value: Path){
		this.value = value;
	}
	public get isTruthy(): never{
		throw new Error('Shouldn\'t be checking the truthiness of a divert target');
	}

	public Cast(newType: ValueType): Value<any>{
		if (newType == this.valueType)
			return this;

		throw this.BadCastException(newType);
	}
	public toString(){
		return 'DivertTargetValue(' + this.targetPath + ')';
	}
}

export class VariablePointerValue extends Value<string>{
	public _contextIndex: number;

	constructor(variableName: string, contextIndex: number = -1){
		super(variableName);

		this._contextIndex = contextIndex;
	}

	public get contextIndex(){
		return this._contextIndex;
	}
	public set contextIndex(value: number) {
		this._contextIndex = value;
	}
	public get variableName(){
		if (this.value === null) return throwNullException('Value.value');
		return this.value;
	}
	public set variableName(value: string){
		this.value = value;
	}
	public get valueType() {
		return ValueType.VariablePointer;
	}

	public get isTruthy(): never{
		throw new Error("Shouldn't be checking the truthiness of a variable pointer");
	}

	public Cast(newType: ValueType): Value<any>{
		if (newType == this.valueType)
			return this;

		throw this.BadCastException(newType);
	}
	public toString(){
		return 'VariablePointerValue(' + this.variableName + ')';
	}
	public Copy(){
		return new VariablePointerValue(this.variableName, this.contextIndex);
	}
}

export class ListValue extends Value<InkList>{
	public get isTruthy(){
		if (this.value === null) { return throwNullException('this.value'); }
		return this.value.Count > 0;
	}
	public get valueType() {
		return ValueType.List;
	}
	public Cast(newType: ValueType): Value<any>{
		if (this.value === null) return throwNullException('Value.value');

		if (newType == ValueType.Int) {
			let max = this.value.maxItem;
			if( max.Key.isNull )
			return new IntValue(0);
			else
			return new IntValue(max.Value);
		}
		else if (newType == ValueType.Float) {
			let max = this.value.maxItem;
			if (max.Key.isNull)
			return new FloatValue(0.0);
			else
			return new FloatValue(max.Value);
		}
		else if (newType == ValueType.String) {
			let max = this.value.maxItem;
			if (max.Key.isNull)
			return new StringValue('');
			else {
				return new StringValue(max.Key.toString());
			}
		}

		if (newType == this.valueType) return this;

		throw this.BadCastException(newType);
	}
	constructor();
	constructor(list: InkList);
	constructor(listOrSingleItem: InkListItem, singleValue: number)
	constructor(listOrSingleItem?: InkListItem | InkList, singleValue?: number){
		super(null);

		if (!listOrSingleItem && !singleValue) {
			this.value = new InkList();
		}
		else if (listOrSingleItem instanceof InkList) {
			this.value = new InkList(listOrSingleItem);
		}
		else if (listOrSingleItem instanceof InkListItem && typeof singleValue === 'number') {
			this.value = new InkList({
				Key: listOrSingleItem,
				Value: singleValue,
			});
		}
	}
	// @ts-ignore
	public static RetainListOriginsForAssignment(oldValue: InkObject | null, newValue: InkObject){
		let oldList = asOrNull(oldValue, ListValue);
		let newList = asOrNull(newValue, ListValue);

		if (newList && newList.value === null) return throwNullException('newList.value');
		if (oldList && oldList.value === null) return throwNullException('oldList.value');

		// When assigning the empty list, try to retain any initial origin names
		if (oldList && newList && newList.value!.Count == 0)
			newList.value!.SetInitialOriginNames(oldList.value!.originNames);
	}
}

export enum ValueType {
	Int = 0,
	Float = 1,
	List = 2,
	String = 3,
	DivertTarget = 4,
	VariablePointer = 5,
}
