"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildBasicAuthenticationHeader = exports.initialize = exports.registerSerializerFor = exports.getSerializerFor = exports.applyPropertySerializers = exports.hydrateSubjectParams = exports.buildUrl = exports.omit = exports.extractPrimitiveProperties = exports.headersFrom = exports.smashMergeConfg = exports.resolveHomeDir = exports.serializeJsonFile = exports.deserializeJsonFile = exports.TypeName = void 0;
const path = require("path");
const fs = require("fs");
const os = require("os");
const commentJson = require("comment-json");
const querystring = require("querystring");
const contracts = require("@codiac.io/codiac-common/contracts");
const _ = require("lodash");
const urlPattern = require("url-pattern");
const inversify_1 = require("inversify");
const codiac_common_1 = require("@codiac.io/codiac-common");
const contracts_1 = require("@codiac.io/codiac-common/contracts");
class TypeName {
    static endpointSpec(entityType) {
        if (typeof entityType == "string") {
            return `endpointSpec<${entityType}>`;
        }
        else {
            return `endpointSpec<${(0, codiac_common_1.typeNameOf)(entityType)}>`;
        }
    }
    // TODO: Promote this to a common/shared library
    static arrayOf(entityType) {
        if (typeof entityType == "string") {
            return `Array<${entityType}>`;
        }
        else {
            return `Array<${(0, codiac_common_1.typeNameOf)(entityType)}>`;
        }
    }
    static criteriaFor(entityType) {
        if (typeof entityType == "string") {
            return `ICriteria<${entityType}>`;
        }
        else {
            return `ICriteria<${(0, codiac_common_1.typeNameOf)(entityType)}>`;
        }
    }
}
exports.TypeName = TypeName;
const endpointSpecs = new inversify_1.Container();
const serializers = new inversify_1.Container();
// TODO:  Promote this to a shared pkg
/** Returns the instantiated object or array.
 * Returns undefined for both an empty or missing source file.
 * Uses JSON.parse and does not perform any explicit casting.
 * Throws encountered exceptions.  Resolves tilde (~) path prefixes.
 * @param pathName Fully qualified path and filename of the json file.
 * @param allowComments (default: false) Flag to use a json comment parser to support comments in the json file.
 */
function deserializeJsonFile(pathName, allowComments = false) {
    try {
        let targetPathName = resolveHomeDir(pathName);
        if (!path.isAbsolute(targetPathName)) {
            throw new Error(`Cannot deserialize: the pathName [${pathName}] is not a fully qualified path.`);
        }
        if (fs.existsSync(targetPathName)) {
            let json = fs.readFileSync(targetPathName, { encoding: 'utf-8' });
            if (allowComments) {
                let obj = commentJson.parse(json); // TSC error forced <unknown>
                return obj;
            }
            else {
                let obj = JSON.parse(json);
                return obj;
            }
        }
        else {
            return undefined;
        }
    }
    catch (err) {
        throw err;
    }
}
exports.deserializeJsonFile = deserializeJsonFile;
/** Uses JSON.stringify() to write the given object to a file.  Resolves tilde (~) path prefixes.
 * @param subject The object to serialize.
 * @param pathName Fully-qualified path and filename (eg "/users/JoeCool/.config/app-config.json").
 * @param allowComments (default: false) Flag to use a json comment parser to support comments in the json file.
 */
function serializeJsonFile(subject, pathName, allowComments = false) {
    let targetPathName = resolveHomeDir(pathName);
    if (!path.isAbsolute(targetPathName)) {
        throw new Error(`Cannot serialize: the pathName [${pathName}] is not a fully qaulified path`);
    }
    if (allowComments) {
        commentJson.stringify(subject, undefined, '  ');
    }
    else {
        fs.writeFileSync(targetPathName, JSON.stringify(subject, undefined, '  '));
    }
}
exports.serializeJsonFile = serializeJsonFile;
/** Replaces the leading tilde (~) in the given file path
 * with the absolute path to the os.homedir (eg: "/Users/JoeCool").
 * NOTE:  Relative paths will slide through this function untouched:
 * this function does not necessarily return an absolute path! */
function resolveHomeDir(pathName) {
    if (pathName.startsWith("~")) {
        return path.join(os.homedir(), pathName.slice(2));
    }
    else {
        return pathName;
    }
}
exports.resolveHomeDir = resolveHomeDir;
/**  */
function smashMergeConfg(defaults, configsInPriorityOrder) {
    if (!configsInPriorityOrder || configsInPriorityOrder.length == 0)
        return defaults;
    else {
        let ultimate = _.assignIn(defaults, ...configsInPriorityOrder.reverse());
        return ultimate;
    }
}
exports.smashMergeConfg = smashMergeConfg;
function headersFrom(options, existingHeaders) {
    let output;
    // FROM the api base...
    /** The name of the http header used to designate the csv list of properties to include in the output signature */
    const HEADER_KEY_PROJECTION_INCLUDES = "output-includes";
    /** The name of the http header used to designate the csv list of properties to include in the output signature */
    const HEADER_KEY_PROJECTION_EXCLUDES = "output-excludes";
    if (options === null || options === void 0 ? void 0 : options.projection) {
        output = Object.assign({}, (existingHeaders !== null && existingHeaders !== void 0 ? existingHeaders : {}));
        output[HEADER_KEY_PROJECTION_INCLUDES] = options.projection.pathsIncluded.join(",");
        output[HEADER_KEY_PROJECTION_EXCLUDES] = options.projection.pathsExcluded.join(",");
    }
    return output;
}
exports.headersFrom = headersFrom;
function extractPrimitiveProperties(remaining) {
    if (remaining == undefined)
        return undefined;
    let prim = {};
    for (let prop in remaining) {
        if (remaining[prop] === null || (typeof remaining[prop] !== 'object' && typeof remaining[prop] !== 'function')) {
            prim[prop] = remaining[prop];
        }
    }
    return prim;
}
exports.extractPrimitiveProperties = extractPrimitiveProperties;
function omit(entity, paths) {
    let output = Object.assign({}, entity);
    for (let path of paths) {
        if (output[path]) {
            delete output[path];
        }
    }
    return output;
}
exports.omit = omit;
function buildUrl(route, request, applyRemainingToQuerystring = false) {
    let pattern = new urlPattern(route, { segmentValueCharset: 'a-zA-Z0-9-_~ %.@' });
    let url = pattern.stringify(request);
    // Remove the properties of the request that are
    // already tokenized in the route 
    let tokenPattern = /(?!:)\w+/gi; //  /(:[^/]\w+)/g
    let rx = new RegExp(tokenPattern, "g");
    let matches = rx.exec(route);
    let remaining = Object.assign({}, request);
    let tokens = [];
    while (matches) {
        // let tokens = matches.map(t => t.slice(1));
        tokens.push(...matches);
        // TODO:  Revisit this to remove token modifiers () 
        // This does not remove the modifiers to a token.  It assumes tokens are all in format ":property"
        remaining = omit(remaining, tokens);
        matches = rx.exec(route);
    }
    let qs;
    if (applyRemainingToQuerystring) {
        // By now we must already have applied any applicable serializers.
        // Whatever properties still remain unserialized are meant for the body, 
        // and cannot occur in the querystring,
        // so we dont add anything to the querystring that isnt already a primitive,
        // or this method will empty it out, eg:  "?myval="
        let natives = extractPrimitiveProperties(remaining);
        qs = querystring.stringify(natives);
    }
    if (qs) {
        url = url.concat("?", qs);
    }
    return url;
}
exports.buildUrl = buildUrl;
/** Injects any property values from the request into subject parameters by name.
 *
 * This method DOES NOT mutate the request object. */
function hydrateSubjectParams(subject, request) {
    var _a;
    let pattern = new urlPattern(subject, { segmentValueCharset: 'a-zA-Z0-9-_~ %.@' });
    let url = pattern.stringify(request);
    if (request == undefined)
        return subject;
    /** prefix indicating that a subject segment is a variable/parameter.
     * eg:  `the.doctor.said.my.:blank.looks.unhealthy` */
    const PARAM_PREFIX = ":";
    if (!subject.includes(PARAM_PREFIX)) {
        return subject;
    }
    else {
        const segments = subject.split(".");
        for (let index = 0; index < segments.length; index++) {
            if ((_a = segments[index]) === null || _a === void 0 ? void 0 : _a.startsWith(PARAM_PREFIX)) {
                let propertyName = segments[index].substring(PARAM_PREFIX.length); // remove the prefix
                if (propertyName != "")
                    segments[index] = request[propertyName];
            }
        }
        return segments.join(".");
    }
}
exports.hydrateSubjectParams = hydrateSubjectParams;
function applyPropertySerializers(entity, serializerMap) {
    let output = entity;
    for (let prop in serializerMap) {
        let propType = serializerMap[prop];
        if (propType && entity[prop]) {
            let ser = getSerializerFor(propType);
            if (ser) {
                output[prop] = ser.serialize(entity[prop]);
            }
        }
    }
    return output;
}
exports.applyPropertySerializers = applyPropertySerializers;
/** Returns undefined when no serializer has been registered for the given valueType. */
function getSerializerFor(valueType) {
    let typeKey = "";
    if (typeof valueType == "string") {
        typeKey = valueType;
    }
    else {
        typeKey = (0, codiac_common_1.typeNameOf)(valueType);
    }
    let ser;
    if (serializers.isBoundNamed(codiac_common_1.INTERFACES.ISerializer, typeKey)) {
        ser = serializers.getNamed(codiac_common_1.INTERFACES.ISerializer, typeKey);
    }
    return ser;
}
exports.getSerializerFor = getSerializerFor;
/** For special serializers (eg: StringFilter, etc).
 * Maps the given serializer for use on the given type
 * when later converting input to an http request.
 * NOTE:  a default serializer will be used if reqd and none has been registered here.
 */
function registerSerializerFor(valueType, serializer) {
    let typeKey = "";
    if (typeof valueType == "string") {
        typeKey = valueType;
    }
    else {
        typeKey = (0, codiac_common_1.typeNameOf)(valueType);
    }
    // All this bc inversify's bind command wont overwrite and results in ambiguous bindings 
    if (serializers.isBoundNamed(codiac_common_1.INTERFACES.ISerializer, typeKey)) {
        // I think this DROPS ALL the other named bindings for this service identifier...
        serializers.rebind(codiac_common_1.INTERFACES.ISerializer)
            .toConstantValue(serializer)
            .whenTargetNamed(typeKey);
    }
    else {
        serializers.bind(codiac_common_1.INTERFACES.ISerializer)
            .toConstantValue(serializer)
            .whenTargetNamed(typeKey);
    }
}
exports.registerSerializerFor = registerSerializerFor;
function initialize(endpointsJson, logger) {
    // MarkStartHere("This may be the only part of this class that makes this client unique. \
    // Perhaps this should be loaded form a json file?  Can we pass types in that way?");
    // // We can do it like this
    // this.endpointSpecs.bind<endpointSpec<Asset>>(TypeName.endpointSpec(Asset)).toConstantValue(
    //   new endpointSpec<Asset>(Asset, httpRestVerb.POST, "/assets/new")
    // );
    // // or we can do it like this
    // let createMany = { 
    // entityType: Array,
    // httpMethod: httpRestVerb.POST,
    // route: "/assets"
    // };
    // // TODO: this isnt binding by httpRestVerb - it needs to be - perhaps use named bindings?
    // let typeName = TypeName.endpointSpec(TypeName.arrayOf(Asset));
    // this.endpointSpecs.bind<endpointSpec<Array<Asset>>>(typeName).toConstantValue(createMany);
    // // can we do it like this?  (this would let us keep it all in a generated or even retrieved json file)
    // let updateJson = '{ "entityType": "Asset", "httpMethod": "POST", "route": "/assets" }';
    // let update = JSON.parse(updateJson);
    // this.endpointSpecs.bind(TypeName.endpointSpec(update.entityType)).toConstantValue(update);
    registerSerializerFor(contracts.StringFilter, new contracts_1.StringFilterExpressionSerializer(true));
    // this.registerSerializerFor<contracts.StringListFilter>(contracts.StringListFilter, contracts.StringListFilter.toExpression);
    // this.registerSerializerFor<contracts.NumericFilter>(contracts.NumericFilter, contracts.NumericFilter.toExpression);
    // this.registerSerializerFor<contracts.NumericListFilter>(contracts.NumericListFilter, contracts.NumericListFilter.toExpression);
    for (let index in endpointsJson) {
        let spec = endpointsJson[index]; // This is dodging type safety and assumes data integrity in the json file.  We should cast
        let nameKey = spec.httpMethod.toLowerCase();
        if (logger)
            logger.trace(`Adding endpoint spec to client endpoint registry: ${spec.entityType}, '${nameKey} ${spec.route}'`);
        endpointSpecs
            .bind(TypeName.endpointSpec(spec.entityType)) // Consider .properBind()
            .toConstantValue(spec)
            .whenTargetNamed(nameKey);
    }
}
exports.initialize = initialize;
/**  Produces the value for the "Authorization" http header in an RFC 2617 Basic Authentication scheme.
 * Consists of the string "Basic " followed by a colon-separated, base64 encoded string of the username and password.
 * eg:  for userid "Aladdin" and password "open sesame", this function will return "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==".
 * @see https://tools.ietf.org/html/rfc2617#page-4 RFC 2617 HTTP Authentication - Assembling the header value for a basic authentication request. */
function buildBasicAuthenticationHeader(userName, passwordOrApiKey) {
    let creds = [userName, passwordOrApiKey].join(":");
    let credsEncoded = Buffer.from(creds).toString('base64');
    let headerValue = `Basic ${credsEncoded}`;
    return headerValue;
}
exports.buildBasicAuthenticationHeader = buildBasicAuthenticationHeader;
//# sourceMappingURL=utils.js.map