interface ContentDifferentialComparisonResult {
    matching : Array<Thing | typeof Thing | ContentDifferential | ContentMarker>;
    unmatched : Array<Thing | typeof Thing | ContentDifferential | ContentMarker>;
}

class ContentDifferential {
    public nouns : Array<Thing | typeof Thing | ContentDifferential | ContentMarker> = [];
    public score : number = 0;

    public constructor (...nouns : Array<any>) {
        this.addNoun(...nouns);
    }

    public addNoun (...nouns : Array<any>) {
        nouns.forEach(noun => {
            this.nouns.push(noun);
        });
        this.score = this.getScore();
        return this;
    }

    public getNouns () {
        return this.nouns;
    }
	
	public replaceNouns (...nouns : Array<any>) {
		this.nouns = nouns;
		return this;
	}

    public isMatch (cd : ContentDifferential, allowPartial = false) {
        let check = this.getUnmatched(cd);
        if ((allowPartial || check.unmatched.length == 0) && check.matching.length == 0) {
            return true;
        } else if (check.matching.length == 0) {
            for (let i = 0; i < check.unmatched.length; i++) {
                if (!(check.unmatched[i] instanceof ContentMarker)) {
                    return false;
                } else {
                    if ((<ContentMarker> check.unmatched[i]).isImportant()) {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    public getUnmatched (cd : ContentDifferential) : ContentDifferentialComparisonResult {
        let unmatched = cd.nouns.slice();
        let matching = this.nouns.slice();
        for (let i = matching.length - 1; i >= 0; i--) {
            for (let k = unmatched.length - 1; k >= 0; k--) {
                if (ContentDifferential.compareNouns(matching[i], unmatched[k])) {
                    unmatched.splice(k, 1);
                    matching.splice(i, 1);
                    break;
                }
            }
        }
        return {
            matching : matching,
            unmatched : unmatched
        };
    }

    public getScore () {
        let highest = 0;
        let count = this.nouns.length;

        this.nouns.forEach((noun) => {
            let level = ContentDifferential.getNounLevel(noun);
            if (highest < level) {
                highest = level;
            }
        });

        return highest + (count / 100);
    }

    public static getNounLevel (noun : Thing | typeof Thing | ContentDifferential | ContentMarker) {
        if (noun == undefined || noun == null) {
            return 0;
        } else if (typeof noun == "function") {
            if (<any>noun.prototype instanceof Thing) {
                let specifity = 2; // Vague Thing
                let parentClass = Object.getPrototypeOf(noun);
                while (parentClass != Thing) {
                    specifity += 0.1;
                    parentClass = Object.getPrototypeOf(parentClass);
                }
                return specifity;
            } else {
                return 2.5; // It's not a "Thing", so it's probably a weird class, which is kind of specific
            }
        } else if (noun instanceof Thing) {
            return 4; // Specific thing
        } else if (noun instanceof ContentDifferential) {
            return 1; // Minor thing
        } else {
            return 0.5;
        }
    }

    public static compareNouns (a : Thing | typeof Thing | ContentDifferential | ContentMarker, b : Thing | typeof Thing | ContentDifferential | ContentMarker) {
        if (a == undefined || a == null) {
            return true;
        }
        if (typeof a == "function") {
            // b must inherit a or be a
            return b == a || b instanceof a || (typeof b == "function" && (<any>b).prototype instanceof a)
        } else if (a instanceof Thing) {
            // b must be a
            return b == a;
        }
        return a === b;
    }

    public static isMatch (matchFrom : Array<ContentDifferential>, matchAgainst : Array<ContentDifferential>) {
        let unmatched = matchAgainst.slice();
        let matching = matchFrom.slice();
        for (let i = matching.length - 1; i >= 0; i--) {
            for (let k = unmatched.length - 1; k >= 0; k--) {
                if (matching[i].isMatch(unmatched[k])) {
                    unmatched.splice(k, 1);
                    matching.splice(i, 1);
                    break;
                }
            }
        }
        if (unmatched.length == 0 && matching.length == 0) {
            return true;
        } else if (unmatched.length == 0) {
            // Check if the only thing missing are unimportant ContentMarkers
            for (let i = 0; i < matching.length; i++) {
                let nouns = matching[i].getNouns();
                for (let k = 0; k < nouns.length; k++) {
                    if (!(nouns[k] instanceof ContentMarker)) {
                        return false;
                    } else {
                        if ((<ContentMarker> nouns[k]).isImportant()) {
                            return false;
                        }
                    }
                }
            }
            // Nothing important found
            return true;
        }
        return false;
    }
}