'use strict';

const _ = require('lodash');
const fsUtils = require('paradox-utils').fs;
const express = require('express');
const Schema = require('paradox-utils').Schema;
const ControllerRequest = require('./controller-request');
const Promise = require('bluebird');
const js2xmlparser = require("js2xmlparser");
const yaml = require('js-yaml');
const fs = require('fs');
const path = require('path');
const RestfulController = require('./restful-controller');
const filterable = require("filterable");
const logger = require('paradox-logger')('api-manager');
const __ = require('paradox-utils');
const ControllerResponse = require('./controller-response');

class ApiManager {

    /**
     * @param {ControllerManager} controller
     * @param {express} server
     * @param cloud
     * @param options
     */
    constructor(controller, server, cloud, options) {
        this.options = _.defaults(options, {
            prefix: null,
            snippets: [],
            endpoints: [],
            basedir: null,
            auth_user: null,
            auth_password: null,
            defaultMaxAge: 24 * 60 * 60, // 1 day
        });

        this.validMethods = ['checkout', 'copy', 'delete', 'get', 'head', 'lock', 'merge', 'mkactivity', 'mkcol', 'move', 'm-search', 'notify', 'options', 'patch', 'post', 'purge', 'put', 'report', 'search', 'subscribe', 'trace', 'unlock', 'unsubscribe'];

        this.cloud = cloud;
        this.controller = controller;
        this.server = server;

        this.endpoints = [];

        this.router = null;
    }

    register() {
        this.registerSchema();
        this.registerCustomRouter();

        this.options.endpoints.forEach(endpoint => {
            endpoint = endpoint.replace(/\\/g, '/');

            if(endpoint.endsWith('/*')) {
                this.registerEndpointDir(endpoint.replace(/\/\*$/, ''));

            } else {
                this.registerEndpointFile(endpoint);
            }
        });

        return Promise.resolve();
    }

    /**
     * Custom router sorts routes to be more specific
     * @returns {ApiManager}
     */
    registerCustomRouter() {
        let Router = express.Router;
        let me = this;

        let MyRouter = function() {
            let instance = Router.apply(this, arguments);
            let handle = instance.handle.bind(instance);

            instance.handle = function(req, res, out) {
                this.stack = me.sortRoutes(this.stack);
                return handle(req, res, out);
            };

            return instance;
        };

        let router = MyRouter();

        this.router = router;
        this.server.use(router);

        return this;
    }

    registerEndpointDir(dir) {
        fsUtils.listFolderFiles(dir).forEach(file => {
            this.registerEndpointFile(file);
        });
    }

    registerEndpointFile(file) {
        let endpoints = require(file);

        let baseOptions = {};

        _.forEach(endpoints, (options, name) => {
            if(name.match(/^options(\..+?)?$/)) {
                baseOptions = _.defaults({}, options, baseOptions);
                return;
            }

            if(name == 'endpoints') {
                // TODO: Poner esto en limpio
                _.forEach(options, options => {
                    if(_.isArray(options)) {
                        _.forEach(options, (options) => {
                            let opts = _.defaults({}, options, baseOptions);
                            this.registerEndpoint(name, opts);
                        });

                    } else {
                        let opts = _.defaults({}, options, baseOptions);
                        this.registerEndpoint(name, opts);
                    }
                });

                return;
            }

            if(_.isArray(options)) {
                _.forEach(options, (options) => {
                    let opts = _.defaults({}, options, baseOptions);
                    this.registerEndpoint(name, opts);
                });

            } else {
                let opts = _.defaults({}, options, baseOptions);
                this.registerEndpoint(name, opts);
            }
        });
    }

    /**
     * Register endpoint that returns the schema
     */
    registerSchema() {
        let router = express.Router();
        let prefix = this.options.prefix || '/';

        // TODO: convertir algunos tipos como regex

        router.get('/schema-definition', (req, res) => {
            let endpoints = this.endpoints.map(endpoint => {
                return this.populateSnippets(endpoint, {
                    host: req.headers['host'] || null,
                    auth_user: this.options.auth_user,
                    auth_password: this.options.auth_password,
                });
            });

            endpoints = _.cloneDeep(endpoints);

            endpoints.forEach(endpoint => {

                endpoint.options.group = endpoint.options.group || 'API';

                // mock
                if(endpoint.options.mock) {
                    endpoint.mock = [];

                    if(typeof endpoint.options.mock == 'string') {
                        endpoint.options.mock = [{
                            name: 'main',
                            path: endpoint.options.mock,
                        }];
                    }

                    _.forEach(endpoint.options.mock, mock => {
                        let code = mock.code || null;

                        if(mock.path) {
                            let fpath = this.options.basedir ? path.resolve(this.options.basedir, mock.path) : mock.path;

                            if(!fs.existsSync(fpath)) {
                                throw new Error('mock file doesn\'t found: ' + fpath);
                            }

                            code = JSON.parse(fs.readFileSync(fpath, 'utf8'));
                        }

                        endpoint.mock.push({
                            name: mock.name || null,
                            // path: mock.path || null,
                            code,
                        })
                    });

                } else {
                    endpoint.mock = null;
                }

                if(endpoint.options.response && endpoint.options.response.fields) {
                    _.forEach(endpoint.options.response.fields, value => {
                        // type
                        if(typeof value.type == 'function') {
                            value.type = value.type();
                        }

                        value.type = value.type.toString();
                    });
                }

                _.forEach(endpoint.options.request, (value, key) => {
                    value.key = key;
                    value.group = value.group || 'default';
                    value.example = value.example || null;

                    // type
                    if(typeof value.type == 'function') {
                        value.type = value.type();
                    }

                    value.type = value.type.toString();
                });
            });

            res
                .status(200)
                .json(endpoints);
        });

        this.server.use(prefix, router);
    }

    populateSnippets(endpoint, options) {
        let snippets = {};

        _.forEach(this.options.snippets, (cb, key) => {
            snippets[key] = cb.apply(this, [endpoint, options]) || null;
        });

        endpoint.snippets = snippets;
        return endpoint;
    }

    /**
     * Register each endpoint
     *
     * @param url
     * @param options
     */
    registerEndpoint(url, options) {
        options = this.normalizeEndpointOpts(options);

        // TODO: Register Schema

        if(options.type == 'rest') {
            this.registerRestEndpoint(url, options);
            return;
        }

        // Fixes
        if(options.request) {
            _.forEach(options.request, (field, key) => {
                field.source = field.source || 'query';
                field.type = field.type || Schema.type.STRING;
                field.hasDefaultValue = 'defaultValue' in field;
            });
        }

        if(options.response) {
            if(!options.response.type) {
                options.response.type = [
                    'application/json',
                    'application/xml',
                    'application/x-yaml',
                ];

            } else {
                if(!_.isArray(options.response.type)) {
                    options.response.type = [options.response.type];
                }
            }
        }


        // Prefijo
        let prefix = [];

        let base = options.base;

        if(base === null) {
            base = this.options.prefix;
        }

        if(base) {
            prefix.push(base);
        }

        if(options.prefix) {
            prefix.push(options.prefix);
        }

        prefix = prefix.join('/').replace(/\/+/, '/');

        let finalUrl = prefix + url + (options.suffix || '');

        this.endpoints.push({
            url,
            finalUrl: finalUrl,
            options,
        });

        // Register metodos
        if(_.isArray(options.method)) {
            options.method = options.method[0];
        }

        if(!options.method) {
            throw new Error('not defined method');
        }

        options.method = options.method.toLowerCase();

        if(!this.validMethods.includes(options.method)) {
            throw new Error('invalid http method: ' + options.method);
        }

        this.router[options.method](finalUrl,
            (req, res, next) => {
                if(!req.isAuthenticated() && !options.public) {
                    let error = new Error('unauthorized');
                    error.silence = true;

                    this.sendApiOperationalError(req, res, error, options);
                    return;
                }

                let routerOpts = _.cloneDeep(options);

                switch(options.type) {
                    case 'controller':
                        this.runController(req, res, next, routerOpts);
                        break;
                    case 'callback':
                        this.runCallback(req, res, next, routerOpts);
                        break;
                    case 'rest':
                        this.runRest(req, res, next, routerOpts);
                        break;
                    // case 'proxy':
                    default:
                        this.sendApiOperationalError(req, res, new Error('type "' + routerOpts.type + '" not implemented'), options);
                }
            });
    }

    checkSecurity(req, res, next) {


        next();
    }

    registerRestEndpoint(url, options) {
        let actions = [];
        let restActions = options.methods || ['list', 'fetch', 'create', 'update', 'delete'];

        let definedActions = options.restMethods || [];

        options.rest = true;

        restActions.forEach(restAction => {
            let opts = _.defaults({
                type: 'controller',
                key: restAction,
                parent: ('rest.' + url).replace(/[^a-zA-Z0-9]/g, '-'),
                request: {},

            }, options, {
                modelName: 'registro',
                modelNamePlural: 'registros',
                suffix: null,
                model: null,
                response: {
                    fields: {},
                }
            });

            if(!opts.model) {
                throw new Error('no model defined for api: ' + url);
            }

            let postBody = false;

            let model = opts.model;
            let Model = null;
            let ModelPromise = null;

            let adapter = null;

            if(typeof model == 'string') {
                let modelParts = model.split(/\./);

                if(modelParts.length != 2) {
                    throw new Error('model must have two parts: ADAPTER.MODEL-NAME');
                }

                adapter = modelParts[0];
                let modelName = modelParts[1];

                Model = this.cloud(adapter).model(modelName, null, {sync: true});
                ModelPromise = this.cloud(adapter).model(modelName, null);

            } else {
                Model = model;
                ModelPromise = Promise.resolve(model);

                adapter = '??';

                opts.model = 'internal';
            }

            let attributes = {};

            if(adapter == 'sql') {
                _.forEach(Model.attributes, (field, key) => {
                    attributes[key] = field;
                });

            } else {
                _.forEach(Model.attributes, (field, key) => {
                    attributes[key] = field;
                });
            }

            if(restAction == 'list') {
                opts.method = 'get';
                opts.name = 'Listado de ' + opts.modelNamePlural;

                opts.action = 'list';

                opts.request['where'] = {
                    source: 'query',
                    type: Schema.type.STRING,
                    description: 'Filtrar lista mediante estas condiciones.',
                    allowNull: true,
                    get(value) {
                        // TODO: parsear filtro
                        if(!value) {
                            return {};
                        }

                        if(_.isPlainObject(value)) {
                            return value;
                        }

                        try {
                            return JSON.parse(value);

                        } catch(e) {
                        }

                        let fields = {};

                        _.forEach(Model.attributes, (field, key) => {
                            if(Schema.type.NUMBER.isFamily(field.type)) {
                                fields[key] = {
                                    type: Number,
                                };
                            }

                            // TODO: ???
                            if(Schema.type.BOOLEAN.isFamily(field.type)) {
                                fields[key] = {
                                    type: Boolean,
                                };
                            }

                            // TODO: ???
                            if(Schema.type.DATE.isFamily(field.type)) {
                                fields[key] = {
                                    type: Date,
                                };
                            }
                        });

                        let querySearch = value.replace(/:\s+?/g, ':');

                        let query = filterable
                            .Query(querySearch, {
                                fields,
                            })
                            .parse();

                        let where = query.toMongo();
                        delete where.tags;

                        return where;
                    },
                    validate: {
                        isValidFilter(value) {
                            if(!_.isPlainObject(value)) {
                                throw new Error('invalid filter format');
                            }

                            _.forEach(value, (value, key) => {
                                if(!(key in attributes)) {
                                    throw new Error('field "' + key + '" not exists in the model');
                                }
                            });
                        },
                    }
                };

                opts.request['properties'] = {
                    source: 'query',
                    type: Schema.type.STRING,
                    description: 'Listado de campos a devolver.',
                    allowNull: true,
                    get(value) {
                        if(typeof value == 'string' && value) {
                            return value.split(/\s*,\s*/g);
                        }

                        return value || [];
                    },
                    validate: {
                        isValidPropertyList(value) {
                            if(!_.isArray(value)) {
                                throw new Error('properties must be an array of fields');
                            }

                            value.forEach(property => {
                                if(typeof property != 'string') {
                                    throw new Error('property must be a string');
                                }

                                if(!property.match(/^[a-z0-9_]+$/i)) {
                                    throw new Error('property name must be alphanumeric');
                                }
                            });
                        }
                    }
                };

                opts.request['order'] = {
                    source: 'query',
                    type: Schema.type.STRING,
                    description: 'Campos por los que ordenar los resultados.',
                    allowNull: true,
                    get(value) {
                        if(!value) {
                            return [];
                        }

                        let body = [];

                        try {
                            body = JSON.parse(value);

                            if(!_.isArray(body)) {
                                return body || [];
                            }

                            if(body.length > 0 && typeof body[0] == 'string') {
                                body = [
                                    body,
                                ];
                            }

                        } catch(e) {
                            if(value) {
                                if(typeof value === 'string' && value) {
                                    let match = value.match(/^([a-z0-9_]+)\s*,\s*(asc|desc)$/i);

                                    if(match) {
                                        body = [
                                            [match[1], match[2].toLowerCase()]
                                        ];

                                    } else {
                                        body = [
                                            [value, 'asc']
                                        ];
                                    }
                                }
                            }
                        }

                        return body || [];
                    },
                    validate: {
                        isValidOrder(value) {
                            if(!_.isArray(value)) {
                                return false;
                            }

                            value.forEach(order => {
                                if(order.length > 0 && typeof order[0] != 'string') {
                                    throw new Error('invalid column type');
                                }

                                if(!order[0].match(/^([a-z0-9_]+)$/i)) {
                                    throw new Error('invalid column name');
                                }

                                if(order.length > 2) {
                                    throw new Error('invalid number of arguments inside a order clause');
                                }

                                if(typeof order[1] != 'string') {
                                    throw new Error('invalid column sort type');
                                }

                                if(!order[1].match(/^(asc|desc)$/)) {
                                    throw new Error('invalid sort direction');
                                }
                            });
                        }
                    }
                };

                opts.request['offset'] = {
                    source: 'query',
                    type: Schema.type.NUMBER,
                    description: 'Nº de registros a saltar en el resultado.',
                    allowNull: true,
                    defaultValue: 0,
                    validate: {
                        min: 0,
                    }
                };

                opts.request['limit'] = {
                    source: 'query',
                    type: Schema.type.NUMBER,
                    description: 'Nº de registros máximos que se devolveran.',
                    allowNull: true,
                    defaultValue: 10,
                    validate: {
                        min: 0,
                        max: 1000,
                    }
                };
            }

            if(restAction == 'create') {
                opts.method = 'post';
                opts.name = 'Crea un ' + opts.modelName;

                opts.action = 'create';

                postBody = true;
            }

            if(restAction == 'update') {
                opts.method = 'put';
                opts.name = 'Modifica un ' + opts.modelName;
                opts.suffix = '/:id';

                opts.action = 'update';

                postBody = true;
            }

            if(restAction == 'fetch') {
                opts.method = 'get';
                opts.name = 'Recupera los detalles de un ' + opts.modelName;
                opts.suffix = '/:id';

                opts.action = 'fetch';
            }

            if(restAction == 'delete') {
                opts.method = 'delete';
                opts.name = 'Elimina un ' + opts.modelName;
                opts.suffix = '/:id';

                opts.action = 'destroy';
            }

            if(opts.suffix == '/:id') {
                // TODO: Sacar el tipo del KEY principal

                opts.request['id'] = {
                    type: Schema.type.STRING,
                    allowNull: false,
                    source: 'params',
                    example: '123',
                };
            }

            if(postBody) {
                _.forEach(attributes, (field, key) => {
                    // if(field.primaryKey) {
                    //     return;
                    // }

                    if(field.internal) {
                        return;
                    }

                    opts.request[key] = Object.assign({}, field, {
                        source: 'body'
                    });
                });
            }

            // mocks
            let mockExample = {};

            _.forEach(attributes, (field, key) => {
                opts.response.fields[key] = field;

                let generatedVal = null;

                if(Schema.type.DATE.isFamily(field.type)) {
                    generatedVal = new Date();

                } else if(Schema.type.FLOAT.isFamily(field.type)) {
                    generatedVal = _.random(0, 1000) + Math.random();

                } else if(Schema.type.NUMBER.isFamily(field.type)) {
                    generatedVal = _.random(0, 1000);

                } else if(Schema.type.STRING.isFamily(field.type)) {
                    generatedVal = 'lorem ipsum';

                } else if(Schema.type.BOOLEAN.isFamily(field.type)) {
                    generatedVal = !!_.random(0, 1);

                } else if(field.type == 'KEY') { // TODO: No entra
                    generatedVal = _.random(1000000000000, 9000000000000);
                }

                mockExample[key] = field.example || field.defaultValue || generatedVal;
            });

            let response = mockExample;

            if(restAction == 'list') {
                response = [];

                for(let i = 0; i < 10; i++) {
                    response.push(mockExample);
                }

            } else if(restAction == 'delete') {
                response = null; // TODO: definir una
            }

            if(response) {
                opts.mock = [
                    {
                        name: 'default',
                        code: response
                    }
                ];
            }

            // controller
            if(!opts.controller) {
                let controller = new RestfulController(null, ModelPromise);

                if(opts.partition) {
                    if(typeof opts.partition == 'function') {
                        controller.on('model', opts.partition);

                    } else {
                        controller.on('model', (model, request) => {
                            let partitionValue = __.object.queryGet(request, opts.partition);

                            if(typeof partitionValue == 'undefined') {
                                throw new Error('partition value not found in request');
                            }

                            return model.partition(partitionValue);
                        });
                    }
                }

                opts.controller = this
                    .controller
                    .getSummoner()
                    .registerController(null, controller);
            }

            _.defaultsDeep({}, definedActions.find(a => a.key == restAction) || {}, opts);

            this.registerEndpoint(url, opts);
        });

        options.actions = actions;
    }

    validateAndRun(req, res, next, options, callback) {
        options = _.defaults(options, {
            controller: null,
            action: null,
        });

        let data = this.collectData(req, options.request);

        let schema = new Schema(options.request, data, {
            validate: options.validate,
        });

        let request = ControllerRequest.factory(req);

        request['params'] = {};
        request['body'] = {};
        request['query'] = {};

        _.forEach(options.request, (field, key) => {
            let source = field.source || 'query';
            request[source][key] = schema.get(key);
        });

        // TODO: esto es peligroso
        options = this.replaceParams(options, req.query);
        options = this.replaceParams(options, req.params);
        options = this.replaceParams(options, req.body);

        request.controller = options.controller;
        request.action = options.action;

        // 1. primero validamos el esquema
        return schema
            .validate()
            .then(() => {
                // 2. llamamos a la función

                return this
                    .injectRequest(callback, request, {schema})
                    .then(
                        result => {
                            this.sendApiResult(req, res, request, result, options);
                        },
                        err => {
                            this.sendApiError(req, res, request, err, options);
                        }
                    );
            })
            .catch(err => {
                this.sendValidationError(req, res, err, options);
            });
    }

    runCallback(req, res, next, options) {
        return this.validateAndRun(req, res, next, options, (request, schema) => {
            return Promise.try(() => {
                return this.injectRequest(options.callback, request);
            });
        });
    }

    runController(req, res, next, options) {
        return this.validateAndRun(req, res, next, options, (request, schema) => {
            return this
                .controller
                .call(request, {
                    timeout: options.timeout
                });
        });
    }

    runRest(req, res, next, options) {
        res.status(500).end('Not implemented');
    }

    injectRequest(callback, request, others) {
        let toInject = _.defaults(
            {},
            {request},
            request.params,
            request.query,
            request.body,
            others
        );

        return __.reflection.applyMapping(callback, this, toInject);
    }

    /* -------------------------------------------------------------------------
     * Utils
     * ---------------------------------------------------------------------- */

    collectData(req, fields) {
        let data = {};

        _.forEach(fields, (field, fieldName) => {
            let source = field.source || 'query';

            if(_.isArray(source)) {
                source = source[0];
            }

            if(fieldName in req[source]) {
                data[fieldName] = req[source][fieldName];

                if(data[fieldName] === '' || typeof data[fieldName] == 'undefined') {
                    data[fieldName] = null;
                }
            }
        });

        return data;
    }

    replaceParams(options, params) {
        for(let key in options) {
            if(typeof options[key] == 'string') {
                for(let pKey in params) {
                    options[key] = options[key].replace(':' + pKey, params[pKey]);
                }
            }
        }

        return options;
    }

    normalizeEndpointOpts(options) {
        return _.defaults(options, {
            // base
            type: 'controller',

            // description
            name: null,
            description: null,

            priority: 0,

            legend: null,
            legend_description: null,

            // security
            public: false,

            // timeout
            timeout: null,

            // TODO: define throttle
            throttle: null,

            // routing
            base: null,
            prefix: null,
            suffix: null,
            method: 'get',
            // controller: null,
            // action: null,
            request: {},

            // validation
            validate: {},

            response: {
                type: [
                    'application/json',
                    'application/xml',
                    'application/x-yaml',
                ],
                codes: [],
                fields: {},
            }
        });
    }

    /* -------------------------------------------------------------------------
     * Output
     * ---------------------------------------------------------------------- */

    extractTypeFromReq(req) {
        let accept = req.header('accept') || 'application/json';

        if(accept.match(/json$/i)) {
            return 'json';

        } else if(accept.match(/xml$/i)) {
            return 'xml';

        } else if(accept.match(/ya?ml$/i)) {
            return 'yaml';
        }

        return null;
    }

    sendOutput(req, res, code, body, options) {
        let type = this.extractTypeFromReq(req);

        res.set('x-api-success', body.success ? 'true' : 'false');

        for(let header in body.request) {
            res.set('x-api-' + header, body.request[header] || '');
        }

        let callback = null;

        // response
        if(options.response) {
            callback = options.response.middleware || null;

            // cache
            let cache = options.response.cache;

            if(cache === false) {
                res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate');
                res.set('Expires', '-1');
                res.set('Pragma', 'no-cache');

            } else if(cache) {
                if(cache === true) {
                    res.set('Cache-Control', 'public, max-age=' + this.options.defaultMaxAge);

                } else if(_.isNumber(cache)) {
                    res.set('Cache-Control', 'public, max-age=' + cache);

                } else if(_.isString(cache)) {
                    res.set('Cache-Control', cache);

                } else if(_.isFunction(cache)) {
                    res.set('Cache-Control', __.reflection.applyMapping(cache, this, {code, body, options, req, res}));

                } else {
                    throw new Error('unknown cache value type');
                }
            }

            // headers
            if(options.response.headers) {
                _.forEach(options.response.headers, (value, name) => {
                    if(typeof value === 'function') {
                        value = __.reflection.applyMapping(value, this, {code, body, options, req, res});
                    }

                    res.set(name, value);
                });
            }
        }

        if(body.result instanceof ControllerResponse) {
            res.status(body.result.code || 200);

            _.forEach(body.result.headers, (value, name) => {
                res.set(name, value);
            });

            callback && __.reflection.applyMapping(callback, this, {code, body, options, req, res});

            res.send(body.result.body);
            return;
        }

        body = body.success ? body['result'] : body['error'];
        callback && __.reflection.applyMapping(callback, this, {code, body, options, req, res});

        if(type == 'xml') {
            res
                .status(code)
                .set('Content-Type', 'application/xml')
                .send(js2xmlparser.parse('response', body));

        } else if(type == 'yaml') {
            res
                .status(code)
                .set('Content-Type', 'application/x-yaml')
                .send(yaml.dump(body));

        } else {
            res
                .status(code)
                .json(body);
        }
    }

    sendApiResult(req, res, request, result, options) {
        this.sendOutput(req, res, 200, {
            success: true,
            result: result instanceof ControllerResponse ? result : __.object.simplify(result),
            request: {
                session: request.session,
                elapsed: request.elapsed() / 1000,
            }
        }, options);
    }

    sendApiError(req, res, request, err, options) {
        let body = err;

        if(err instanceof Error) {
            // if(!err.silence) {
            //     logger.exception(err);
            // }

            if(err.code == 404 && !err.httpStatus) {
                err.httpStatus = 404;
            }

            body = {
                success: false,
                error: {
                    code: err.code || null,
                    type: err.type || null,
                    message: err.httpMessage || err.message,
                    // trace: (err.stack + "").split(/\n+/),
                },
                request: {
                    session: request ? request.session : null,
                    elapsed: request ? request.elapsed() / 1000 : null,
                }
            };
        }

        this.sendOutput(req, res, err.httpStatus || 500, body, options);
    }

    sendApiOperationalError(req, res, err, options) {
        return this.sendApiError(req, res, null, err, options);
    }

    sendValidationError(req, res, err, options) {
        this.sendApiOperationalError(req, res, err, options);
    }

    /* -------------------------------------------------------------------------
     * Utils
     * ---------------------------------------------------------------------- */

    sortRoutes(routes) {
        let sortable = routes
            .map((object, index) => {
                let route = object.route.path;

                return {
                    index,
                    object,
                    route,
                    priority: index,
                    regex: this.routeToRegex(route),
                    isRegex: route.match(/:|{/),
                    test: route.replace(/:[a-zA-Z0-9_]+/g, 'test')
                }
            });

        // 1. search conflicts
        sortable.forEach(a => {
            sortable.forEach(b => {
                if(!a.isRegex || !b.isRegex) {
                    return;
                }

                if(a.route == b.route) {
                    return;
                }

                if(a.isRegex && b.isRegex) {
                    let aMatch = !!a.regex.exec(b.test);
                    let bMatch = !!b.regex.exec(a.test);

                    if(aMatch && bMatch) {
                        logger.warn('route conflict detected between: ' + a.route + ' and ' + b.route);
                    }
                }
            });
        });

        // 2. put priorities
        sortable.forEach(a => {
            sortable.forEach(b => {
                if(a.route == b.route) {
                    return;
                }

                if(a.regex.exec(b.test)) {
                    if(a.priority < b.priority) {
                        a.priority = b.priority + 0.0001;
                    }
                }
            });
        });

        sortable.sort((a, b) => {
            return a.priority < b.priority ? -1 : 1;
        });

        let sorted = sortable
            .map(route => {
                return route.object;
            });

        return sorted;
    }

    routeToRegex(route) {
        let PATH_REGEXP = new RegExp(["(\\\\.)", "([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?", "([.+*?=^!:${}()[\\]|\\/])"].join("|"), "g");

        function escapeGroup(e) {
            return e.replace(/([=!:$\/()])/g, "\\$1")
        }

        function attachKeys(e, r) {
            return e.keys = r, e;
        }

        function doFlags(e) {
            return e.sensitive ? "" : "i"
        }

        function replacePath(e, r) {
            function a(e, a, n, i, p, o, u, c) {
                if(a) return a;
                if(c) return "\\" + c;
                let s = "+" === u || "*" === u,
                    g = "?" === u || "*" === u;
                return r.push({
                    name: i || t++,
                    delimiter: n || "/",
                    optional: g,
                    repeat: s
                }), n = n ? "\\" + n : "", p = escapeGroup(p || o || "[^" + (n || "\\/") + "]+?"), s && (p = p + "(?:" + n + p + ")*"), g ? "(?:" + n + "(" + p + "))?" : n + "(" + p + ")"
            }

            let t = 0;
            return e.replace(PATH_REGEXP, a)
        }

        function pathToRegexp(path, replace, flags) {
            replace = [];
            flags = {};

            let t = flags.strict,
                n = flags.end !== !1,
                i = replacePath(path, replace),
                p = "/" === path.charAt(path.length - 1);

            return t || (i = (p ? i.slice(0, -2) : i) + "(?:\\/(?=$))?"), i += n ? "$" : t && p ? "" : "(?=\\/|$)", attachKeys(new RegExp("^" + i, doFlags(flags)), replace)
        }

        return pathToRegexp(route);
    }
}

ApiManager.snippet = {
    BASE: require('./snippets/base-snippet'),
    CSHARP: require('./snippets/csharp-snippet'),
    CURL: require('./snippets/curl-snippet'),
    JAVA: require('./snippets/java-snippet'),
    JQUERY: require('./snippets/jquery-snippet'),
    NODEJS: require('./snippets/nodejs-snippet'),
    PHP: require('./snippets/php-snippet'),
    PYTHON: require('./snippets/python-snippet'),
    RUBY: require('./snippets/ruby-snippet'),
};

module.exports = ApiManager;