/// <reference path="../Relation.ts" />
/// <reference path="../../../Functions.ts" />
class RelationHandlerStrictOneToMany extends RelationHandler {

    public setRelation (relation : Relation, left : any, right : any, value? : any) : boolean {
        // a LEFT must not circle in on itself
        // Is right any of the left's parents?
        let parents = this.getAllLeft(left);
        if (parents.indexOf(right) != -1) {
            console.warn("[Relation] Attempt to create a circular relation:", this, left, right);
            return false; // Allowing this would create a circular relation
        }

        // from this point on we'll guarantee that the new relation can be made.
        // Changes will occur, so "return false" is no longer acceptable.
        // a RIGHT can only point to a single LEFT
        this.unsetRight(right);

        return true;
    }

    public unset (thing : any) {
        this.unsetLeft(thing);
        this.unsetRight(thing);
    }

    public unsetLeft (left: any) {
        this.relations.forEach(relation => {
            relation.unsetLeft(left);
        });
    }

    public unsetRight (right : any) {
        this.relations.forEach(relation => {
            relation.unsetRight(right);
        });
    }

    // A many can only have one one
    public getLeft (right : any) {
        let result;
        for (let i = 0; i < this.relations.length; i++) {
            result = this.relations[i].getLeft(right);
            if (result != undefined) {
                return result;
            }
        }
    }

    /**
     * This returns all ones that are relevant.
     * So in a relationship like A -> B -> C and asking for C, this will return
     * B because it's related to C and then A because that's related to B.
     * @param right
     * @returns {Array}
     */
    public getAllLeft (right : any) {
        let newParent = this.getLeft(right);
        let parents = [];
        while (newParent != undefined ) {
            parents.push(newParent);
            newParent = this.getLeft(newParent);
        }
        return parents;
    }

    public getAllLeftType (right : any, type : any) {
        return this.getAllLeft(right).filter(left => {
            return left instanceof type;
        });
    }

    /**
     * Returns the highest One that can be found.
     * for A -> B -> C and asking for C, this would return A
     * as that's the one "originating" the other relations.
     * @param right
     * @returns {any}
     */
    public getLastLeft (right : any) {
        let parent = this.getLeft(right);
        let newParent = this.getLeft(parent);
        while (newParent != undefined) {
            parent = newParent;
            newParent = this.getLeft(parent);
        }
        return parent;
    }

    public getAllRight (left : any) {
        let rights = [];
        this.relations.forEach(relation => {
            rights.push(...relation.getRight(left));
        });
        arrayUnique(rights);
        return rights;
    }

    public getAllRightTypes (left : any, rightType : any) {
        return this.getAllRight(left).filter(right => {
            return right instanceof rightType;
        });
    }
}

class RelationOneToMany extends Relation {
    private valuesHash : {[idid : string] : any} = {};
    private oneMap = new Map<any, Array<any>>();
    private manyMap = new Map<any, any>();

    public getValue (left : any, right : any) {
        return this.valuesHash[Relation.createString(left, right)];
    }

    /**
     * Only relationships between Objets can have values
     * @param left
     * @param right
     * @param value
     */
    public setValue (left : any, right : any, value : any) {
        if (this.manyMap.get(right) == left) {
            let string = Relation.createString(left, right);
            if (string != undefined) {
                this.valuesHash[string] = value;
            }
        }
    }

    public setRelation (left : any, right : any, value? : any) {
        let proceed;
        if (this.handler != undefined) {
            proceed = this.handler.setRelation(this, left, right, value);
        }

        if (proceed) {
            // something on the left can't be on the right
            this.unsetRight(left);

            // something on the right can't be on the left
            this.unsetLeft(right);

            // something on the right can only have one left
            this.unsetRight(right);

            let string = Relation.createString(left, right);
            if (string != undefined) {
                this.valuesHash[string] = value;
            }
            this.manyMap.set(right, left);
            if (this.oneMap.get(left) == undefined) {
                this.oneMap.set(left, [right]);
            } else {
                this.oneMap.get(left).push(right);
            }
        }
    }

    public getLeft (right : any) {
        return this.manyMap.get(right);
    }

    public getAnyLeft () {
        let left = [];
        this.oneMap.forEach((value, key) => {
            left.push(key);
        });
        arrayUnique(left);
        return left;
    }

    public getAnyRight () {
        let rights = [];
        this.oneMap.forEach((value) => {
            rights.push(...value);
        });
        arrayUnique(rights);
        return rights;
    }

    public getAnyRightType (type : any) {
        return this.getAnyRight().filter(value => {
            return value instanceof type;
        });
    }

    public isRight (left : any, needle : any) {
        let right = this.oneMap.get(left);
        if (right != undefined) {
            return right.indexOf(needle) != -1;
        }
        return false;
    }

    public getRight (left : any) {
        let right = this.oneMap.get(left);
        if (right != undefined) {
            return [].concat(...right);
        }
        return [];
    }

    public getRightType (left : any, type : any) {
        return this.getRight(left).filter(right => {
            return right instanceof type;
        });
    }

    public getRightTypeOne (left : any, type : any) {
        let rights = this.getRightType(left, type);
        if (rights.length > 0) {
            return rights[0];
        }
    }

    public getRelationValue (left : any, right : any) {
        return this.valuesHash[Relation.createString(left, right)];
    }

    public unsetRight (right : any) {
        let left = this.manyMap.get(right);
        if (left != undefined) {
            let allRight = this.oneMap.get(left);
            allRight.splice(allRight.indexOf(right), 1);
            this.manyMap.delete(right);
            if (allRight.length == 0) {
                this.oneMap.delete(left);
            }
            let string = Relation.createString(left, right);
            if (string != undefined) {
                delete(this.valuesHash[string]);
            }
        }
    }

    public unsetLeft (left : any) {
        let allRight = this.oneMap.get(left);
        if (allRight != undefined) {
            allRight.forEach((right : any) => {
                this.manyMap.delete(right);
                let string = Relation.createString(left, right);
                if (string != undefined) {
                    delete(this.valuesHash[string]);
                }
            });
            this.oneMap.delete(left);
        }
    }
}