import {ApiQuery, ApiQueryScope, QueryStep} from "./ApiQuery";
import {ApiEdgeQuery} from "../edge/ApiEdgeQuery";
import {ApiEdgeQueryContext} from "../edge/ApiEdgeQueryContext";
import {ApiEdgeRelation} from "../relations/ApiEdgeRelation";
import {ApiEdgeError} from "./ApiEdgeError";
import {ApiEdgeQueryFilter, ApiEdgeQueryFilterType} from "../edge/ApiEdgeQueryFilter";
import {
    ApiRequest,
    ApiRequestType,
    EdgePathSegment,
    EntryPathSegment,
    MethodPathSegment,
    PathSegment,
    RelatedFieldPathSegment
} from "../request/ApiRequest";
import {ApiEdgeQueryResponse} from "../edge/ApiEdgeQueryResponse";
import {ApiEdgeQueryType} from "../edge/ApiEdgeQueryType";
import {OneToOneRelation} from "../relations/OneToOneRelation";
import {Api} from "../Api";
import {ApiEdgeMethod, ApiEdgeMethodOutput, ApiEdgeMethodScope} from "../edge/ApiEdgeMethod";
import {ApiEdgeAction, ApiEdgeActionTrigger, ApiEdgeActionTriggerKind} from "../edge/ApiEdgeAction";
import {ApiAction, ApiActionTriggerKind} from "./ApiAction";
import {ApiEdgeDefinition} from "../edge/ApiEdgeDefinition";
import {OneToManyRelation} from "../relations/OneToManyRelation";

const parse = require('obj-parse');
const debug = require('debug')('api-core');

export class EmbedQueryQueryStep implements QueryStep {
    query: ApiQuery;
    request: ApiRequest;
    segment: PathSegment;
    sourceField: string;
    targetField: string;
    idField: string;
    forceArray: boolean;
    isMultiMulti: boolean;

    constructor(query: ApiQuery, segment: PathSegment, request: ApiRequest) {
        this.query = query;
        this.query.request = this.request = request;
        this.segment = segment;

        if(!this.segment.relation) throw new Error('Invalid relation provided.');
        this.sourceField = this.segment.relation.relationId;
        this.targetField = this.segment.relation.name;
        this.idField = this.segment.relation.relatedId;
        this.forceArray = this.segment.relation instanceof OneToManyRelation;
        this.isMultiMulti = this.segment.relation.hasPair;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());
            if(scope.response) {
                const target = scope.response.data;

                if(Array.isArray(target)) {
                    const targetIndex: { [key: string]: any[] } = {},
                        targetArrayIndex: { [key: string]: { entry: any, index: number }[] } = {},
                        ids: string[] = [];

                    for(let entry of target) {
                        const id = entry[this.sourceField];
                        if(id) {
                            if(Array.isArray(id)) {
                                let index = 0;
                                for(let _id of id) {
                                    if (targetArrayIndex[_id]) targetArrayIndex[_id].push({ entry, index });
                                    else targetArrayIndex[_id] = [{entry, index}];
                                    ids.push(_id);
                                    index++
                                }
                                entry[this.sourceField] = [];
                            }
                            else {
                                if (targetIndex[id]) targetIndex[id].push(entry);
                                else targetIndex[id] = [entry];
                                ids.push(id);
                                if (this.forceArray)
                                    entry[this.targetField] = [];
                            }
                        }
                    }

                    this.request.context.filters = [
                        new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.In, ids)
                    ];

                    this.query.execute(scope.identity).then((response) => {
                        if(response.data && response.data.length) {
                            for (let entry of response.data) {
                                let ids = entry[this.idField];
                                if(!Array.isArray(ids)) {
                                    ids = [ids];
                                }

                                for(let id of ids) {
                                    if (targetIndex[id]) {
                                        for (let subEntry of targetIndex[id]) {
                                            if (this.forceArray)
                                                subEntry[this.targetField].push(entry);
                                            else
                                                subEntry[this.targetField] = entry;
                                        }
                                    }
                                    if(targetArrayIndex[id]) {
                                        for (let { entry: subEntry, index } of targetArrayIndex[id]) {
                                            subEntry[this.targetField][index] = entry;
                                        }
                                    }
                                }
                            }
                        }
                        resolve(scope)
                    }).catch(reject);
                }
                else {
                    const sourceId = target[this.sourceField];

                    if(!sourceId) {
                        resolve(scope);
                        return
                    }

                    if(Array.isArray(sourceId)) {
                        this.request.context.filters = [
                            new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.In, sourceId)
                        ];
                    }
                    else if(this.forceArray) {
                        this.request.context.filters = [
                            new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.Equals, sourceId)
                        ];
                    }
                    else {
                        //Now we can replace TBD and provide a real id for the query.
                        (this.segment as EntryPathSegment).id = sourceId;
                    }

                    this.query.execute(scope.identity).then((response) => {
                        if (Array.isArray(sourceId)) {  // restore original order of array items
                            const unordered_data = response.data;
                            response.data = [];
                            for (const id of sourceId) {
                                const item = unordered_data.find((item:any) => {
                                    return item.id.toString() == id.toString();
                                });
                                if (item)
                                    response.data.push(item);
                                else
                                    console.warn("WARNING: can\'t find in embed results this id: " + id);
                            }
                        }
                        target[this.targetField] = response.data;
                        resolve(scope)
                    }).catch(e => {
                        console.warn(e);
                        resolve(scope)
                    })
                }
            }
            else resolve(scope)
        })
    };

    inspect = () => `EMBED QUERY /${this.sourceField} -> ${this.targetField}`;
}

export class QueryEdgeQueryStep implements QueryStep {
    query: ApiEdgeQuery;

    constructor(query: ApiEdgeQuery) {
        this.query = query;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(this.query.type !== ApiEdgeQueryType.Get && this.query.type !== ApiEdgeQueryType.List) {
                this.query.body = scope.body;
            }

            this.query.context = scope.context;
            this.query.context.populatedRelations = []; // prevent embed step to run on external query --- it shall only run at the original executor
            this.query.context.identity = scope.identity;

            this.query.execute().then((response) => {
                scope.context = new ApiEdgeQueryContext();
                scope.response = response;
                resolve(scope)
            }).catch(reject);
        })
    };

    inspect = () => `QUERY /${this.query.edge.pluralName}`;
}

export class CallMethodQueryStep implements QueryStep {
    method: ApiEdgeMethod;
    edge: ApiEdgeDefinition;

    constructor(method: ApiEdgeMethod, edge: ApiEdgeDefinition) {
        this.method = method;
        this.edge = edge;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());
            scope.context.method = this.method.name;

            this.method.execute(scope)
                .then((response) => {
                    scope.response = response;
                    resolve(scope)
                }).catch((e) => {
                debug(`failed to execute ${this.method.name} method`, e);
                reject(e)
            });
        })
    };

    inspect = () => `call{${this.method.name}}`;
}

export class RelateQueryStep implements QueryStep {
    relation: ApiEdgeRelation;

    constructor(relation: ApiEdgeRelation) {
        this.relation = relation;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry"));
            scope.context.filter(this.relation.relationId, ApiEdgeQueryFilterType.Equals, scope.response.data[this.relation.relatedId]);
            resolve(scope);
        })
    };

    inspect = () => `RELATE ${this.relation.relationId} = ${this.relation.relatedId}`;
}

export class RelateBackwardsQueryStep implements QueryStep {
    relation: ApiEdgeRelation;

    constructor(relation: ApiEdgeRelation) {
        this.relation = relation;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry"));
            scope.context.filter(this.relation.relatedId, ApiEdgeQueryFilterType.Equals, scope.response.data[this.relation.relationId]);
            resolve(scope);
        })
    };

    inspect = () => `RELATE ${this.relation.relatedId} = ${this.relation.relationId}`;
}

export class RelateChangeQueryStep implements QueryStep {
    relation: ApiEdgeRelation;

    constructor(relation: ApiEdgeRelation) {
        this.relation = relation;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(!scope.body) return reject(new ApiEdgeError(404, "Missing Body"));
            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry"));
            parse(this.relation.relationId).assign(
                scope.body,
                scope.response.data[this.relation.relatedId].toString());
            resolve(scope);
        })
    };

    inspect = () => `RELATE CHANGE ${this.relation.relationId}`;
}

export class RelateBackwardsChangeQueryStep implements QueryStep {
    relation: ApiEdgeRelation;

    constructor(relation: ApiEdgeRelation) {
        this.relation = relation;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(!scope.body) return reject(new ApiEdgeError(404, "Missing Body"));
            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry"));
            parse(this.relation.relatedId).assign(
                scope.body,
                scope.response.data[this.relation.relationId].toString());
            resolve(scope);
        })
    };

    inspect = () => `RELATE CHANGE BACKWARD ${this.relation.relatedId}`;
}

/*export class CheckResponseQueryStep implements QueryStep {
    execute = (scope: QueryScope) => {
        return new Promise((resolve, reject) => {
            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry"));
            resolve(scope);
        })
    };

    inspect = () => `CHECK`;
}

export class NotImplementedQueryStep implements QueryStep {
    description: string;

    constructor(description: string) {
        this.description = description;
    }

    execute = (scope: QueryScope) => {
        return new Promise(resolve => {
            resolve(scope);
        })
    };

    inspect = () => `NOT IMPLEMENTED: ${this.description}`;
}*/

export class SetResponseQueryStep implements QueryStep {
    response: ApiEdgeQueryResponse;

    constructor(response: ApiEdgeQueryResponse) {
        this.response = response;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise(resolve => {
            debug(`[${scope.query.id}]`, this.inspect());

            scope.response = this.response;
            scope.context = new ApiEdgeQueryContext();
            resolve(scope);
        })
    };

    inspect = () => `SET RESPONSE`;
}

export class SetBodyQueryStep implements QueryStep {
    body: any;
    stream: NodeJS.ReadableStream|null;

    constructor(body: any, stream: NodeJS.ReadableStream|null = null) {
        this.body = body;
        this.stream = stream
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise(resolve => {
            debug(`[${scope.query.id}]`, this.inspect());

            scope.body = this.body;
            scope.stream = this.stream;
            resolve(scope);
        })
    };

    inspect = () => `SET BODY`;
}

export class ProvideIdQueryStep implements QueryStep {
    fieldName: string;

    constructor(fieldName: string = Api.defaultIdField) {
        this.fieldName = fieldName;
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise((resolve, reject) => {
            debug(`[${scope.query.id}]`, this.inspect());

            if(!scope.response) return reject(new ApiEdgeError(404, "Missing Entry"));
            scope.context.id = scope.response.data[this.fieldName];
            resolve(scope);
        })
    };

    inspect = () => `PROVIDE ID: ${this.fieldName}`;
}

export class ExtendContextQueryStep implements QueryStep {
    context: ApiEdgeQueryContext;

    constructor(context: ApiEdgeQueryContext) {
        this.context = context
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise(resolve => {
            debug(`[${scope.query.id}]`, this.inspect());

            scope.context.id = this.context.id || scope.context.id;
            if(this.context.pagination) {
                scope.context.pagination = this.context.pagination;
            }
            this.context.fields.forEach(f => scope.context.fields.push(f));
            this.context.populatedRelations.forEach(f => scope.context.populatedRelations.push(f));
            this.context.filters.forEach(f => scope.context.filters.push(f));
            this.context.sortBy.forEach(f => scope.context.sortBy.push(f));
            resolve(scope)
        })
    };

    inspect = () => {
        if(this.context.id) {
            return `EXTEND CONTEXT (id=${this.context.id})`
        }
        else {
            return `APPLY PARAMETERS`
        }
    };
}

export class ExtendContextLiveQueryStep implements QueryStep {
    apply: (context: ApiEdgeQueryContext) => void|any;

    constructor(func: (context: ApiEdgeQueryContext) => void|any) {
        this.apply = func
    }

    execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => {
        return new Promise(resolve => {
            debug(`[${scope.query.id}]`, this.inspect());

            this.apply(scope.context);
            resolve(scope)
        })
    };

    inspect = () => {
        return `EXTEND CONTEXT LIVE`
    };
}

/*export class GenericQueryStep implements QueryStep {
    description: string;
    step: () => Promise<QueryScope>;
    context: any;

    constructor(description: string, step: () => Promise<QueryScope>, context: any) {
        this.description = description;
        this.step = step;
        this.context = context;
    }

    execute = (scope: QueryScope) => {
        return this.step.apply(this.context, [ scope ]);
    };

    inspect = () => this.description
}*/

export class ApiQueryBuilder {
    api: Api;

    constructor(api: Api) {
        this.api = api;
    }

    private addQueryActions(triggerKind: ApiEdgeActionTriggerKind,
                            query: ApiQuery,
                            edgeQuery: ApiEdgeQuery,
                            relation: ApiEdgeRelation|null,
                            output: boolean = false) {
        const edge = edgeQuery.edge,
            queryType = edgeQuery.type,
            trigger = relation ?
                ApiEdgeActionTrigger.Relation :
                (output ? ApiEdgeActionTrigger.OutputQuery : ApiEdgeActionTrigger.SubQuery);

        let actions: ApiEdgeAction[];
        if(relation) {
            actions = edge.actions.filter((action: ApiEdgeAction) =>
                action.triggerKind == triggerKind &&
                (action.targetTypes & queryType) &&
                (action.triggers & trigger) &&
                (!action.triggerNames.length || action.triggerNames.indexOf(relation.name) == -1))
        }
        else {
            actions = edge.actions.filter((action: ApiEdgeAction) =>
                action.triggerKind == triggerKind &&
                (action.targetTypes & queryType) &&
                (action.triggers & trigger))
        }

        actions.forEach((action: ApiEdgeAction) => query.unshift(action));

        if(output) {
            const apiTrigger = triggerKind == ApiEdgeActionTriggerKind.BeforeEvent ?
                ApiActionTriggerKind.BeforeOutput : ApiActionTriggerKind.AfterOutput;
            this.api.actions
                .filter((action: ApiAction) => action.triggerKind == apiTrigger)
                .forEach((action: ApiAction) => query.unshift(action))
        }
    }

    private addMethodActions(triggerKind: ApiEdgeActionTriggerKind,
                            query: ApiQuery,
                            method: ApiEdgeMethod,
                            queryType: ApiEdgeQueryType,
                            edge: ApiEdgeDefinition,
                            output: boolean = false) {
        const trigger = ApiEdgeActionTrigger.Method;

        let actions = edge.actions.filter((action: ApiEdgeAction) =>
            action.triggerKind == triggerKind &&
            (action.targetTypes & queryType) &&
            (action.triggers & trigger) &&
            (!action.triggerNames.length || action.triggerNames.indexOf(method.name) == -1));

        actions.forEach((action: ApiEdgeAction) => query.unshift(action));

        if(output) {
            const apiTrigger = triggerKind == ApiEdgeActionTriggerKind.BeforeEvent ?
                ApiActionTriggerKind.BeforeOutput : ApiActionTriggerKind.AfterOutput;
            this.api.actions
                .filter((action: ApiAction) => action.triggerKind == apiTrigger)
                .forEach((action: ApiAction) => query.unshift(action))
        }
    }

    private addMethodCallStep(request: ApiRequest, query: ApiQuery, method: ApiEdgeMethod, edge: ApiEdgeDefinition, output: boolean) {
        if(method.acceptedTypes & request.type) {
            let queryType = ApiEdgeQueryType.Any;
            if (request.type === ApiRequestType.Create) {
                queryType = ApiEdgeQueryType.Create;
            } else if (request.type === ApiRequestType.Read) {
                queryType = ApiEdgeQueryType.Read;
            } else if (request.type === ApiRequestType.Update) {
                queryType = ApiEdgeQueryType.Update;
            } else if (request.type === ApiRequestType.Patch) {
                queryType = ApiEdgeQueryType.Patch;
            } else if (request.type === ApiRequestType.Delete) {
                queryType = ApiEdgeQueryType.Delete;
            } else if (request.type === ApiRequestType.Exists) {
                queryType = ApiEdgeQueryType.Exists;
            } else if (request.type === ApiRequestType.Change) {
                queryType = ApiEdgeQueryType.Change;
            }

            this.addMethodActions(ApiEdgeActionTriggerKind.AfterEvent, query, method, queryType, edge, output);
            query.unshift(new CallMethodQueryStep(method, edge));
            this.addMethodActions(ApiEdgeActionTriggerKind.BeforeEvent, query, method, queryType, edge, output)
        }
        else {
            throw new ApiEdgeError(405, "Method Not Allowed");
        }
    }

    private addQueryStep(query: ApiQuery,
                         step: QueryEdgeQueryStep,
                         relation: ApiEdgeRelation|null = null,
                         output: boolean = false) {
        this.addQueryActions(ApiEdgeActionTriggerKind.AfterEvent, query, step.query, relation, output);
        query.unshift(step);
        this.addQueryActions(ApiEdgeActionTriggerKind.BeforeEvent, query, step.query, relation, output);
    }

    private static buildProvideIdStep(query: ApiQuery, currentSegment: PathSegment): boolean {
        if(currentSegment instanceof EntryPathSegment) {
            query.unshift(new ExtendContextLiveQueryStep(context => context.id = currentSegment.id));
            return false
        }
        else if(currentSegment instanceof RelatedFieldPathSegment) {
            query.unshift(new ProvideIdQueryStep(currentSegment.relation.relationId));
            return true
        }
        else {
            //TODO: Add support for method calls with parameters
            return false
        }
    }

    private buildCheckStep(query: ApiQuery, currentSegment: PathSegment): boolean {
        //STEP 1: Create the check query.
        //TODO: Check this code...
        if(currentSegment instanceof EntryPathSegment) {
            query.unshift(new SetResponseQueryStep(new ApiEdgeQueryResponse({ [currentSegment.edge.idField||Api.defaultIdField]: currentSegment.id })));
            return false
        }
        else if(currentSegment instanceof RelatedFieldPathSegment) {
            this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.relation.to, ApiEdgeQueryType.Get)), currentSegment.relation);
        }
        else {
            //TODO: Add support for method calls (non-base query case)
            throw new ApiEdgeError(500, "Not Implemented")
        }

        //STEP 2: Provide ID for the check query.
        return ApiQueryBuilder.buildProvideIdStep(query, currentSegment)
    }

    private buildReadStep(query: ApiQuery, currentSegment: PathSegment): boolean {
        //STEP 1: Create the read query.
        if(currentSegment instanceof RelatedFieldPathSegment) {
            this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.relation.to, ApiEdgeQueryType.Get)), currentSegment.relation);
        }
        else {
            this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.edge, ApiEdgeQueryType.Get)));
        }

        //STEP 2: Provide ID for the read query.
        return ApiQueryBuilder.buildProvideIdStep(query, currentSegment)
    }

    private buildEmbedSteps(query: ApiQuery, request: ApiRequest, lastSegment: PathSegment) {
        if(request.type === ApiRequestType.Read
            && (lastSegment instanceof EdgePathSegment
                || (lastSegment instanceof MethodPathSegment && lastSegment.method.output === ApiEdgeMethodOutput.List))) {
            for (let relation of request.context.populatedRelations) {
                const segment = new EdgePathSegment(relation.to, relation);

                const embedRequest = new ApiRequest(request.api);
                embedRequest.path.add(segment);

                // We add the step directly directly, as pre- and post-actions are not
                // supported on embed query steps. These actions will be executed as
                // part of the sub-query.
                query.unshift(new EmbedQueryQueryStep(this.build(embedRequest), segment, embedRequest));
            }
        }
        else {
            for (let relation of request.context.populatedRelations) {
                let segment: EdgePathSegment|EntryPathSegment;

                if(relation instanceof OneToManyRelation) {
                    // TODO: Should we specify exactly array relations?
                    segment = new EdgePathSegment(relation.to, relation);
                }
                else {
                    // The id is literally TBD, it is going to be set once we have the data,
                    // what we build now is only an execution plan.
                    segment = new EntryPathSegment(relation.to, 'TBD', relation);
                }

                const embedRequest = new ApiRequest(request.api);
                embedRequest.path.add(segment);

                // We add the step directly, as pre- and post-actions are not
                // supported on embed query steps. These actions will be executed as
                // part of the sub-query.
                query.unshift(new EmbedQueryQueryStep(this.build(embedRequest), segment, embedRequest));
            }
        }
    }

    private buildReadQuery = (request: ApiRequest): ApiQuery => {
        let query = new ApiQuery();

        let segments = request.path.segments,
            lastSegment = segments[segments.length-1];

        //STEP 0: Create embed queries
        this.buildEmbedSteps(query, request, lastSegment);

        //STEP 1: Create the base query which will provide the final data.
        let readMode = true;
        let baseQuery: ApiEdgeQuery;
        if(lastSegment instanceof EdgePathSegment) {
            baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.List);
            this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
        }
        else if(lastSegment instanceof RelatedFieldPathSegment) {
            baseQuery = new ApiEdgeQuery(lastSegment.relation.to, ApiEdgeQueryType.Get);
            this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), lastSegment.relation, true);

        }
        else if(lastSegment instanceof MethodPathSegment) {
            this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true);
            if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) {
                //TODO: Add support for providing id for Edge methods.
                query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField));
            }
            readMode = lastSegment.method.requiresData;
        }
        else {
            baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Get);
            this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
        }

        //STEP 2: Provide context for the base query.
        query.unshift(new ExtendContextQueryStep(request.context));

        //STEP 3: Provide ID for the base query.
        if(lastSegment instanceof EntryPathSegment) {
            const _segment = lastSegment; //Add closure to make sure it won't be overridden later.
            query.unshift(new ExtendContextLiveQueryStep(context => context.id = _segment.id))
        }
        else if(lastSegment instanceof RelatedFieldPathSegment) {
            if(lastSegment.relation.relatedId !== lastSegment.relation.to.idField) {
                query.unshift(new RelateBackwardsQueryStep(lastSegment.relation));
            }
            else {
                query.unshift(new ProvideIdQueryStep(lastSegment.relation.relationId))
            }
        }
        else {
            //TODO: Add support for method calls with parameters
        }

        //STEP 4: Provide filters and validation for the base query.
        for(let i = segments.length-2; i >= 0; i--) {
            let currentSegment = segments[i];

            //STEP 1: Relate to the current query.
            let relation = segments[i+1].relation;
            let edge = segments[i+1].edge;
            if(relation && !(relation instanceof OneToOneRelation)) {
                if(edge === relation.to) {
                    query.unshift(new RelateBackwardsQueryStep(relation));
                }
                else {
                    query.unshift(new RelateQueryStep(relation));
                }
            }

            //STEP 2: Read or Check
            if(readMode) {
                readMode = this.buildReadStep(query, currentSegment)
            }
            else {
                readMode = this.buildCheckStep(query, currentSegment)
            }
        }

        //STEP 5: Add OnInput actions
        this.api.actions
            .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput)
            .forEach((action: ApiAction) => query.unshift(action));

        //STEP 6: Return the completed query.
        return query
    };

    private buildChangeQuery = (request: ApiRequest): ApiQuery => {
        let query = new ApiQuery();

        let segments = request.path.segments,
            lastSegment = segments[segments.length-1],
            readMode = true;

        //STEP 0: Create embed queries
        this.buildEmbedSteps(query, request, lastSegment);

        //STEP 1: Create the base query which will provide the final data.
        let baseQuery: ApiEdgeQuery;
        if(lastSegment instanceof RelatedFieldPathSegment) {
            if(request.type === ApiRequestType.Update) {
                baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Patch);
                request.body = { [lastSegment.relation.relationId]: request.body.id||request.body._id };
                this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
            }
            else if(request.type === ApiRequestType.Patch) {
                baseQuery = new ApiEdgeQuery(lastSegment.relation.to, ApiEdgeQueryType.Patch);
                this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
            }
            else {
                throw new ApiEdgeError(400, "Invalid Delete Query");
            }
        }
        else if(lastSegment instanceof MethodPathSegment) {
            this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true);
            if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) {
                //TODO: Add support for providing id for Edge methods.
                query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField));
            }
            readMode = lastSegment.method.requiresData;
        }
        else {
            if(request.type === ApiRequestType.Update) {
                baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Update);
                this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
            }
            else if(request.type === ApiRequestType.Patch) {
                baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Patch);
                this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
            }
            else {
                baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Delete);
                this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true);
            }
        }

        //STEP 2: Provide context for the base query.
        query.unshift(new ExtendContextQueryStep(request.context));

        //STEP 3: Provide ID for the base query.
        if(lastSegment instanceof EntryPathSegment) {
            const _segment = lastSegment; //Add closure to make sure it won't be overridden later.
            query.unshift(new ExtendContextLiveQueryStep(context => context.id = _segment.id))
        }
        else if(lastSegment instanceof RelatedFieldPathSegment) {
            if(request.type === ApiRequestType.Update) {
                let previousSegment = segments[segments.length-2];
                query.unshift(new ProvideIdQueryStep(previousSegment.edge.idField||Api.defaultIdField));
                readMode = false; //Provide ID from the previous segment without querying the database.
            }
            else {
                query.unshift(new ProvideIdQueryStep(lastSegment.relation.relationId))
            }
        }
        else {
            //TODO: Add support for method calls with parameters
        }

        //STEP 4: Provide filters and validation for the base query.
        for(let i = segments.length-2; i >= 0; i--) {
            let currentSegment = segments[i];

            //STEP 1: Relate to the current query.
            let relation = segments[i+1].relation;
            let edge = segments[i+1].edge;
            if(relation && !(relation instanceof OneToOneRelation)) {
                if(edge === relation.to) {
                    query.unshift(new RelateBackwardsQueryStep(relation));
                }
                else {
                    query.unshift(new RelateQueryStep(relation));
                }

                if(request.type !== ApiRequestType.Delete) {
                    if(edge === relation.to) {
                        query.unshift(new RelateBackwardsChangeQueryStep(relation))
                    }
                    else {
                        query.unshift(new RelateChangeQueryStep(relation))
                    }
                }
            }

            //STEP 2: Read or Check
            if(readMode) {
                readMode = this.buildReadStep(query, currentSegment)
            }
            else {
                readMode = this.buildCheckStep(query, currentSegment)
            }
        }

        //STEP 5: Provide body for the query
        if(request.body || request.stream)
            query.unshift(new SetBodyQueryStep(request.body, request.stream));

        //STEP 6: Add OnInput actions
        this.api.actions
            .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput)
            .forEach((action: ApiAction) => query.unshift(action));

        //STEP 7: Return the completed query.
        return query
    };

    private buildCreateQuery = (request: ApiRequest): ApiQuery => {
        let query = new ApiQuery();

        let segments = request.path.segments,
            lastSegment = segments[segments.length-1],
            readMode = true;

        //STEP 0: Create embed queries
        this.buildEmbedSteps(query, request, lastSegment);

        //STEP 1: Create the base query which will provide the final data.
        if(lastSegment instanceof MethodPathSegment) {
            this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true);
            if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) {
                //TODO: Add support for providing id for Edge methods.
                query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField));
            }
            readMode = lastSegment.method.requiresData;
        }
        else {
            this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Create)));
        }

        //STEP 2: Provide filters and validation for the base query.
        for(let i = segments.length-2; i >= 0; i--) {
            let currentSegment = segments[i];

            //STEP 1: Relate to the current query.
            let relation = segments[i+1].relation;
            let edge = segments[i+1].edge;
            if(relation && !(relation instanceof OneToOneRelation)) {
                if(edge === relation.to) {
                    query.unshift(new RelateBackwardsChangeQueryStep(relation))
                }
                else {
                    query.unshift(new RelateChangeQueryStep(relation))
                }
            }

            //STEP 2: Read or Check
            if(readMode) {
                readMode = this.buildReadStep(query, currentSegment)
            }
            else {
                readMode = this.buildCheckStep(query, currentSegment)
            }
        }

        //STEP 3: Provide context for the base query.
        query.unshift(new SetBodyQueryStep(request.body, request.stream));

        //STEP 4: Add OnInput actions
        this.api.actions
            .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput)
            .forEach((action: ApiAction) => query.unshift(action));

        //STEP 5: Return the completed query.
        return query
    };


    build = (request: ApiRequest): ApiQuery => {
        switch(request.type) {
            case ApiRequestType.Read:
                return this.buildReadQuery(request);
            case ApiRequestType.Update:
            case ApiRequestType.Patch:
            case ApiRequestType.Delete:
                return this.buildChangeQuery(request);
            case ApiRequestType.Create:
                return this.buildCreateQuery(request);
            default:
                throw new ApiEdgeError(400, "Unsupported Query Type")
        }
    }
}
