"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LajiApiError = void 0;
class LajiApiError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
    }
}
exports.LajiApiError = LajiApiError;
const splitAndResolvePath = (path, params) => {
    // parse path into segments based on path parameters
    // eg. "/person/{personToken}/profile" -> ["/person", "/{personToken}", "/profile"]
    const segments = path.split(/(\/\{[^}]+\})/).filter(Boolean);
    if (!params) {
        return segments;
    }
    // resolve path parameters (eg. replace '/{personToken}' with params.path.personToken)
    for (let i = 1; i < segments.length; i++) {
        const segment = segments[i];
        segments[i] = segment.replace(/\{([^}]+)\}/, (match, p1) => { var _a; return (_a = params["path"][p1]) !== null && _a !== void 0 ? _a : ""; });
    }
    return segments;
};
/**
 * ApiClient with automatically generated typings for laji-api. An `ApiClientImplementation` must be provided, that will
 * perform the actual requests. The implementation acts as a middleware for injecting access token and person token.
 *
 * Get requests are automatically cached, and they will flush if the resource receives a POST/PUT/DELETE request.
 */
class ApiClient {
    constructor(apiClient, lang = "en", translations) {
        this.lang = "en";
        this.cacheTree = { branches: new Map(), queries: new Map(), path: "" };
        this.on = {};
        this.apiClient = apiClient;
        this.lang = lang;
        this.translations = translations;
    }
    flush(path, params) {
        var _a;
        const segments = splitAndResolvePath(path, params);
        let node = this.cacheTree;
        const nodesToFlush = [];
        for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            if (node.branches.has(segment)) {
                node = node.branches.get(segment);
                nodesToFlush.push(node);
            }
            else {
                break;
            }
        }
        for (let i = 0; i < nodesToFlush.length; i++) {
            const node = nodesToFlush[i];
            node.queries = new Map();
            (_a = this.on[node.path]) === null || _a === void 0 ? void 0 : _a.forEach(fn => fn());
        }
    }
    getCacheNode(path, params) {
        const segments = splitAndResolvePath(path, params);
        const serializedQuery = hashRecord((params === null || params === void 0 ? void 0 : params.query) || {});
        let node = this.cacheTree;
        for (let i = 0; i < segments.length; i++) {
            const segment = segments[i];
            if (node.branches.has(segment)) {
                node = node.branches.get(segment);
            }
            else {
                const newNode = { branches: new Map(), queries: new Map(), path: segments.join("") };
                node.branches.set(segment, newNode);
                node = newNode;
            }
            if (i === segments.length - 1) {
                continue;
            }
            if (node.queries.has(serializedQuery)) {
                return node;
            }
        }
        return node;
    }
    ;
    /** The response is cached until the resource receives a POST/PUT/DELETE request. */
    get(path_1, params_1) {
        return __awaiter(this, arguments, void 0, function* (path, params, 
        /** Defaults to true */
        useCache = true) {
            let cacheNode;
            let serializedQuery;
            if (useCache) {
                serializedQuery = hashRecord(params === null || params === void 0 ? void 0 : params.query);
                cacheNode = this.getCacheNode(path, params);
                if (cacheNode.queries.has(serializedQuery)) {
                    return cacheNode.queries.get(serializedQuery);
                }
            }
            const result = this.fetch(path, "get", params);
            if (useCache) {
                cacheNode.queries = cacheNode.queries;
                cacheNode.queries.set(serializedQuery, result);
            }
            return result;
        });
    }
    /** Subscribe to a GET request. It re-emits when the resource receives a POST/PUT/DELETE request. */
    subscribe(path, params, onFulfilled, onError, 
    /**
     * Store doesn't sync it's search indices right away after data is changed, so delaying the request can be useful
     */
    delay) {
        if (!this.on[path])
            this.on[path] = [];
        const fn = delay
            ? () => { setTimeout(() => this.get(path, params).then(onFulfilled, onError), delay); }
            : () => { this.get(path, params).then(onFulfilled, onError); };
        this.on[path].push(fn);
        this.get(path, params).then(onFulfilled, onError);
        return () => {
            this.on[path] = this.on[path].filter(_fn => _fn !== fn);
        };
    }
    put(path, params, body) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = yield this.fetch(path, "put", params, body);
            this.flush(path, params);
            return result;
        });
    }
    post(path, params, body) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = this.fetch(path, "post", params, body);
            this.flush(path, params);
            return result;
        });
    }
    delete(path, params) {
        return __awaiter(this, void 0, void 0, function* () {
            const result = this.fetch(path, "delete", params);
            this.flush(path, params);
            return result;
        });
    }
    /**
     * Implementing apiClient must return a promise that passes the raw response as 1st arg.
     * @param path URL for GET.
     * @param query Object, where keys are param names and values are param values.
     * @returns a Promise.
     */
    fetch(path_1, method_1, params_1, body_1) {
        return __awaiter(this, arguments, void 0, function* (path, method, params, body, options = {}) {
            const _query = Object.assign({ lang: this.lang }, ((params === null || params === void 0 ? void 0 : params.query) || {}));
            if (!_query.lang) {
                delete _query.lang;
            }
            const pathSegments = splitAndResolvePath(path, params);
            options = Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({}, (options.headers || {})), { "API-Version": 1, "Accept-Language": this.lang }) });
            if (body && !otherThanJSON(body)) {
                options = Object.assign(Object.assign({}, options), { headers: Object.assign(Object.assign({}, (options.headers || {})), { "Content-Type": "application/json", "Accept": "application/json" }) });
                body = JSON.stringify(body);
            }
            const response = yield this.apiClient.fetch(pathSegments.join(""), ((params === null || params === void 0 ? void 0 : params.query) || {}), Object.assign({ method, body }, options));
            if (response.status >= 400) {
                const error = yield response.json();
                throw new LajiApiError(error === null || error === void 0 ? void 0 : error.message, response.status);
            }
            return (response.headers.get("Content-Type") || "").includes("application/json")
                ? response.json()
                : response.text();
        });
    }
    setLang(lang) {
        this.lang = lang;
        this.cacheTree = { branches: new Map(), queries: new Map(), path: "" };
    }
}
exports.default = ApiClient;
const otherThanJSON = (body) => body instanceof FormData ||
    body instanceof Blob ||
    body instanceof ArrayBuffer ||
    body instanceof URLSearchParams ||
    body instanceof ReadableStream;
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
const sortRecordRecursively = (record) => (Object.entries(record)
    .sort(([aKey, aVal], [bKey, bVal]) => aKey > bKey ? 1 : aKey < bKey ? -1 : 0)
    .reduce((acc, [key, value]) => {
    acc[key] = isRecord(value) ? sortRecordRecursively(value) : value;
    return acc;
}, Object.create(null)));
const hashRecord = (record) => JSON.stringify(sortRecordRecursively(record || {}));
