import {Path} from './Path';
import {Container} from './Container';
import {Debug} from './Debug';
import {asOrNull, asINamedContentOrNull} from './TypeAssertion';
import { throwNullException } from './NullException';
import { SearchResult } from './SearchResult';
import { DebugMetadata } from './DebugMetadata';

export class InkObject{

	public parent: InkObject | null = null;

	get debugMetadata(): DebugMetadata | null {
		if (this._debugMetadata === null) {
			if (this.parent) {
				return this.parent.debugMetadata;
			}
		}

		return this._debugMetadata;
	}

	set debugMetadata(value) {
		this._debugMetadata = value;
	}

	get ownDebugMetadata() {
		return this._debugMetadata;
	}

	private _debugMetadata: DebugMetadata | null = null;

	public DebugLineNumberOfPath(path: Path) {
		if (path === null)
			return null;

		// Try to get a line number from debug metadata
		let root = this.rootContentContainer;
		if (root) {
			let targetContent = root.ContentAtPath(path).obj;
			if (targetContent) {
				let dm = targetContent.debugMetadata;
				if (dm !== null) {
					return dm.startLineNumber;
				}
			}
		}

		return null;
	}

	get path(){
		if (this._path == null) {

			if (this.parent == null) {
				this._path = new Path();
			} else {
				let comps: Path.Component[] = [];

				let child: InkObject = this;
				let container = asOrNull(child.parent, Container);

				while (container !== null) {

					let namedChild = asINamedContentOrNull(child);
					if (namedChild != null && namedChild.hasValidName) {
						comps.unshift(new Path.Component(namedChild.name));
					} else {
						comps.unshift(new Path.Component(container.content.indexOf(child)));
					}

					child = container;
					container = asOrNull(container.parent, Container);
				}

				this._path = new Path(comps);
			}

		}

		return this._path;
	}
	private _path: Path | null = null;

	public ResolvePath(path: Path | null): SearchResult{
		if (path === null) return throwNullException('path');
		if (path.isRelative) {
			let nearestContainer = asOrNull(this, Container);

			if (nearestContainer === null) {
				Debug.Assert(this.parent !== null, "Can't resolve relative path because we don't have a parent");
				nearestContainer = asOrNull(this.parent, Container);
				Debug.Assert(nearestContainer !== null, 'Expected parent to be a container');
				Debug.Assert(path.GetComponent(0).isParent);
				path = path.tail;
			}

			if (nearestContainer === null) { return throwNullException('nearestContainer'); }
			return nearestContainer.ContentAtPath(path);
		} else {
			let contentContainer = this.rootContentContainer;
			if (contentContainer === null) { return throwNullException('contentContainer'); }
			return contentContainer.ContentAtPath(path);
		}
	}

	public ConvertPathToRelative(globalPath: Path){
		let ownPath = this.path;

		let minPathLength = Math.min(globalPath.length, ownPath.length);
		let lastSharedPathCompIndex = -1;

		for (let i = 0; i < minPathLength; ++i) {
			let ownComp = ownPath.GetComponent(i);
			let otherComp = globalPath.GetComponent(i);

			if (ownComp.Equals(otherComp)) {
				lastSharedPathCompIndex = i;
			} else {
				break;
			}
		}

		// No shared path components, so just use global path
		if (lastSharedPathCompIndex == -1)
			return globalPath;

		let numUpwardsMoves = (ownPath.componentCount-1) - lastSharedPathCompIndex;

		let newPathComps: Path.Component[] = [];

		for(let up = 0; up < numUpwardsMoves; ++up)
			newPathComps.push(Path.Component.ToParent());

		for (let down = lastSharedPathCompIndex + 1; down < globalPath.componentCount; ++down)
			newPathComps.push(globalPath.GetComponent(down));

		let relativePath = new Path(newPathComps, true);
		return relativePath;
	}

	public CompactPathString(otherPath: Path){
		let globalPathStr = null;
		let relativePathStr = null;

		if (otherPath.isRelative) {
			relativePathStr = otherPath.componentsString;
			globalPathStr = this.path.PathByAppendingPath(otherPath).componentsString;
		}
		else {
			let relativePath = this.ConvertPathToRelative(otherPath);
			relativePathStr = relativePath.componentsString;
			globalPathStr = otherPath.componentsString;
		}

		if (relativePathStr.length < globalPathStr.length)
			return relativePathStr;
		else
			return globalPathStr;
	}

	get rootContentContainer(){
		let ancestor: InkObject = this;
		while (ancestor.parent) {
			ancestor = ancestor.parent;
		}
		return asOrNull(ancestor, Container);
	}

	public Copy(): InkObject {
		throw Error("Not Implemented: Doesn't support copying");
	}
	// SetChild works slightly diferently in the js implementation.
	// Since we can't pass an objets property by reference, we instead pass
	// the object and the property string.
	// TODO: This method can probably be rewritten with type-safety in mind.
	public SetChild(obj: any, prop: any, value: any){
		if (obj[prop])
			obj[prop] = null;

		obj[prop] = value;

		if( obj[prop] )
			obj[prop].parent = this;
	}
}
