import { decodeCID } from "../../common/cid.js";
import { setup } from "../../setup/internal.js";
import BaseTree from "../base/tree.js";
import PublicFile from "./PublicFile.js";
import PublicHistory from "./PublicHistory.js";
import * as cidLog from "../../common/cid-log.js";
import * as common from "../../common/index.js";
import * as dns from "../../dns/index.js";
import * as check from "../types/check.js";
import * as link from "../link.js";
import * as metadata from "../metadata.js";
import * as pathing from "../../path.js";
import * as protocol from "../protocol/index.js";
import * as skeleton from "../protocol/public/skeleton.js";
import * as versions from "../versions.js";
export class PublicTree extends BaseTree {
    constructor({ links, header, cid }) {
        super();
        this.children = {};
        this.cid = cid;
        this.links = links;
        this.header = header;
        this.history = new PublicHistory(this);
    }
    static async empty() {
        return new PublicTree({
            links: {},
            header: {
                metadata: metadata.empty(false, versions.latest),
                skeleton: {},
            },
            cid: null
        });
    }
    static async fromCID(cid) {
        const info = await protocol.pub.get(cid);
        if (!check.isTreeInfo(info)) {
            throw new Error(`Could not parse a valid public tree at: ${cid}`);
        }
        return PublicTree.fromInfo(info, cid);
    }
    static async fromInfo(info, cid) {
        const { userland, metadata, previous, skeleton } = info;
        const links = await protocol.basic.getFileSystemLinks(decodeCID(userland));
        return new PublicTree({
            links,
            header: { metadata, previous, skeleton },
            cid
        });
    }
    static instanceOf(obj) {
        return common.hasProp(obj, "links")
            && common.hasProp(obj, "header")
            && check.isLinks(obj.links)
            && check.isTreeHeader(obj.header);
    }
    async createChildTree(name, onUpdate) {
        const child = await PublicTree.empty();
        const existing = this.children[name];
        if (existing) {
            if (PublicFile.instanceOf(existing)) {
                throw new Error(`There is a file at the given path: ${name}`);
            }
            else if (!PublicTree.instanceOf(existing)) {
                throw new Error(`Not a public tree at the given path: ${name}`);
            }
            else {
                return existing;
            }
        }
        await this.updateDirectChild(child, name, onUpdate);
        return child;
    }
    async createOrUpdateChildFile(content, name, onUpdate) {
        const existing = await this.getDirectChild(name);
        let file;
        if (existing === null) {
            file = await PublicFile.create(content);
        }
        else if (PublicFile.instanceOf(existing)) {
            file = await existing.updateContent(content);
        }
        else {
            throw new Error(`There is already a directory with that name: ${name}`);
        }
        await this.updateDirectChild(file, name, onUpdate);
        return file;
    }
    async putDetailed() {
        const details = await protocol.pub.putTree(this.links, this.header.skeleton, this.header.metadata, this.cid);
        this.header.previous = this.cid || undefined;
        this.cid = details.cid;
        return details;
    }
    async updateDirectChild(child, name, onUpdate) {
        if (this.readOnly)
            throw new Error("Tree is read-only");
        this.children[name] = child;
        const details = await child.putDetailed();
        this.updateLink(name, details);
        onUpdate && await onUpdate();
        return this;
    }
    removeDirectChild(name) {
        delete this.links[name];
        delete this.header.skeleton[name];
        if (this.children[name]) {
            delete this.children[name];
        }
        return this;
    }
    async getDirectChild(name) {
        let child = null;
        if (this.children[name]) {
            return this.children[name];
        }
        const childInfo = this.header.skeleton[name] || null;
        if (childInfo === null)
            return null;
        // Hard link
        if (check.isSkeletonInfo(childInfo)) {
            const cid = decodeCID(childInfo.cid);
            child = childInfo.isFile
                ? await PublicFile.fromCID(cid)
                : await PublicTree.fromCID(cid);
            // Soft link
        }
        else if (check.isSoftLink(childInfo)) {
            return PublicTree.resolveSoftLink(childInfo);
        }
        // Check that the child wasn't added while retrieving the content from the network
        if (this.children[name]) {
            return this.children[name];
        }
        if (child)
            this.children[name] = child;
        return child;
    }
    async get(path) {
        if (path.length < 1)
            return this;
        const res = await this.getRecurse(this.header.skeleton, path);
        // Hard link
        if (check.isSkeletonInfo(res)) {
            const cid = decodeCID(res.cid);
            const info = await protocol.pub.get(cid);
            return check.isFileInfo(info)
                ? PublicFile.fromInfo(info, cid)
                : PublicTree.fromInfo(info, cid);
        }
        // Child
        return res;
    }
    async getRecurse(skel, path) {
        const head = path[0];
        const child = skel[head] || null;
        const nextPath = skeleton.nextNonEmpty(path);
        if (check.isSoftLink(child)) {
            const resolved = await PublicTree.resolveSoftLink(child);
            if (nextPath) {
                if (PublicTree.instanceOf(resolved)) {
                    return resolved.get(nextPath).then(makeReadOnly);
                }
                else {
                    return null;
                }
            }
            return resolved;
        }
        else if (child === null || nextPath === null) {
            return child;
        }
        else if (child.subSkeleton) {
            return this.getRecurse(child.subSkeleton, nextPath);
        }
        else {
            return null;
        }
    }
    // Links
    // -----
    assignLink({ name, link, skeleton }) {
        this.links[name] = link;
        this.header.skeleton[name] = skeleton;
        this.header.metadata.unixMeta.mtime = Date.now();
    }
    static async resolveSoftLink(link) {
        const [domain, ...pieces] = link.ipns.split("/");
        const path = pathing.fromPosix(pieces.join("/"));
        const isPublic = pathing.isBranch(pathing.Branch.Public, path) ||
            pathing.isBranch(pathing.Branch.Pretty, path);
        if (!isPublic)
            throw new Error("Mixing public and private soft links is not supported yet.");
        const rootCid = domain === await common.authenticatedUserDomain({ withFiles: true })
            ? await cidLog.newest()
            : await dns.lookupDnsLink(domain);
        if (!rootCid)
            throw new Error(`Failed to resolve the soft link: ${link.ipns} - Could not resolve DNSLink`);
        const publicCid = (await protocol.basic.getSimpleLinks(decodeCID(rootCid))).public.cid;
        const publicPath = pathing.removeBranch(path);
        const publicTree = await PublicTree.fromCID(decodeCID(publicCid));
        const item = await publicTree.get(pathing.unwrap(publicPath));
        if (item)
            item.readOnly = true;
        return item;
    }
    getLinks() {
        // add missing metadata into links
        return Object.values(this.links).reduce((acc, cur) => {
            const s = this.header.skeleton[cur.name];
            return {
                ...acc,
                [cur.name]: s && s.isFile !== undefined
                    ? { ...cur, isFile: s.isFile }
                    : { ...cur },
            };
        }, {});
    }
    updateLink(name, result) {
        const { cid, metadata, userland, size, isFile, skeleton } = result;
        this.assignLink({
            name,
            link: link.make(name, cid, false, size),
            skeleton: {
                cid,
                metadata,
                userland,
                subSkeleton: skeleton,
                isFile
            }
        });
        return this;
    }
    insertSoftLink({ name, path, username }) {
        const softLink = {
            ipns: `${username}.files.${setup.endpoints.user}/public/${pathing.toPosix(path)}`,
            name
        };
        this.assignLink({
            name,
            link: softLink,
            skeleton: softLink
        });
        return this;
    }
}
function makeReadOnly(maybeFileOrTree) {
    if (maybeFileOrTree)
        maybeFileOrTree.readOnly = true;
    return maybeFileOrTree;
}
export default PublicTree;
//# sourceMappingURL=PublicTree.js.map