import * as DagCBOR from "@ipld/dag-cbor";
import * as Uint8arrays from "uint8arrays";
import { Branch } from "../../path/index.js";
import { decodeCID, encodeCID } from "../../common/index.js";
import { permissionPaths } from "../../permissions.js";
import * as DAG from "../../dag/index.js";
import * as Identifiers from "../../common/identifiers.js";
import * as Link from "../link.js";
import * as Path from "../../path/index.js";
import * as RootKey from "../../common/root-key.js";
import * as TypeChecks from "../../common/type-checks.js";
import * as Versions from "../versions.js";
import * as protocol from "../protocol/index.js";
import BareTree from "../bare/tree.js";
import MMPT from "../protocol/private/mmpt.js";
import PublicTree from "../v1/PublicTree.js";
import PrivateTree from "../v1/PrivateTree.js";
import PrivateFile from "../v1/PrivateFile.js";
import { PublicRootWasm } from "../v3/PublicRootWasm.js";
// CLASS
export default class RootTree {
    constructor({ dependencies, links, mmpt, privateLog, sharedCounter, sharedLinks, publicTree, prettyTree, privateNodes }) {
        this.dependencies = dependencies;
        this.links = links;
        this.mmpt = mmpt;
        this.privateLog = privateLog;
        this.sharedCounter = sharedCounter;
        this.sharedLinks = sharedLinks;
        this.publicTree = publicTree;
        this.prettyTree = prettyTree;
        this.privateNodes = privateNodes;
    }
    // INITIALISATION
    // --------------
    static async empty({ accountDID, dependencies, rootKey, wnfsWasm }) {
        if (wnfsWasm) {
            dependencies.manners.log(`⚠️ Running an EXPERIMENTAL new version of the file system: 3.0.0`);
        }
        const publicTree = wnfsWasm
            ? await PublicRootWasm.empty(dependencies)
            : await PublicTree.empty(dependencies.depot, dependencies.reference);
        const prettyTree = await BareTree.empty(dependencies.depot);
        const mmpt = MMPT.create(dependencies.depot);
        // Private tree
        const rootPath = Path.toPosix(Path.directory(Path.Branch.Private));
        const rootTree = await PrivateTree.create(dependencies.crypto, dependencies.depot, dependencies.manners, dependencies.reference, mmpt, rootKey, null);
        await rootTree.put();
        // Construct tree
        const tree = new RootTree({
            dependencies,
            links: {},
            mmpt,
            privateLog: [],
            sharedCounter: 1,
            sharedLinks: {},
            publicTree,
            prettyTree,
            privateNodes: {
                [rootPath]: rootTree
            }
        });
        // Store root key
        await RootKey.store({ accountDID, crypto: dependencies.crypto, readKey: rootKey });
        // Set version and store new sub trees
        await tree.setVersion(wnfsWasm ? Versions.wnfsWasm : Versions.latest);
        await Promise.all([
            tree.updatePuttable(Branch.Public, publicTree),
            tree.updatePuttable(Branch.Pretty, prettyTree),
            tree.updatePuttable(Branch.Private, mmpt)
        ]);
        // Fin
        return tree;
    }
    static async fromCID({ accountDID, dependencies, cid, permissions }) {
        const { crypto, depot, manners } = dependencies;
        const links = await protocol.basic.getSimpleLinks(dependencies.depot, cid);
        const keys = permissions ? await permissionKeys(crypto, accountDID, permissions) : [];
        const version = await parseVersionFromLinks(dependencies.depot, links);
        const wnfsWasm = Versions.equals(version, Versions.wnfsWasm);
        if (wnfsWasm) {
            dependencies.manners.log(`⚠️ Running an EXPERIMENTAL new version of the file system: 3.0.0`);
        }
        // Load public parts
        const publicCID = links[Branch.Public]?.cid || null;
        const publicTree = publicCID === null
            ? await PublicTree.empty(dependencies.depot, dependencies.reference)
            : wnfsWasm
                ? await PublicRootWasm.fromCID({ depot, manners }, decodeCID(publicCID))
                : await PublicTree.fromCID(dependencies.depot, dependencies.reference, decodeCID(publicCID));
        const prettyTree = links[Branch.Pretty]
            ? await BareTree.fromCID(dependencies.depot, decodeCID(links[Branch.Pretty].cid))
            : await BareTree.empty(dependencies.depot);
        // Load private bits
        const privateCID = links[Branch.Private]?.cid || null;
        let mmpt, privateNodes;
        if (privateCID === null) {
            mmpt = MMPT.create(dependencies.depot);
            privateNodes = {};
        }
        else {
            mmpt = await MMPT.fromCID(dependencies.depot, decodeCID(privateCID));
            privateNodes = await loadPrivateNodes(dependencies, accountDID, keys, mmpt);
        }
        const privateLogCid = links[Branch.PrivateLog]?.cid;
        const privateLog = privateLogCid
            ? await DAG.getPB(depot, decodeCID(privateLogCid))
                .then(dagNode => dagNode.Links.map(Link.fromDAGLink))
                .then(links => links.sort((a, b) => {
                return parseInt(a.name, 10) - parseInt(b.name, 10);
            }))
            : [];
        // Shared
        const sharedCid = links[Branch.Shared]?.cid || null;
        const sharedLinks = sharedCid
            ? await this.getSharedLinks(depot, decodeCID(sharedCid))
            : {};
        const sharedCounterCid = links[Branch.SharedCounter]?.cid || null;
        const sharedCounter = sharedCounterCid
            ? await protocol.basic
                .getFile(dependencies.depot, decodeCID(sharedCounterCid))
                .then(a => JSON.parse(Uint8arrays.toString(a, "utf8")))
            : 1;
        // Construct tree
        const tree = new RootTree({
            dependencies,
            links,
            mmpt,
            privateLog,
            sharedCounter,
            sharedLinks,
            publicTree,
            prettyTree,
            privateNodes
        });
        if (links[Branch.Version] == null) {
            // Old versions of WNFS didn't write a root version link
            await tree.setVersion(Versions.latest);
        }
        // Fin
        return tree;
    }
    // MUTATIONS
    // ---------
    async put() {
        const { cid } = await this.putDetailed();
        return cid;
    }
    async putDetailed() {
        return protocol.basic.putLinks(this.dependencies.depot, this.links);
    }
    updateLink(name, result) {
        const { cid, size, isFile } = result;
        this.links[name] = Link.make(name, cid, isFile, size);
        return this;
    }
    async updatePuttable(name, puttable) {
        return this.updateLink(name, await puttable.putDetailed());
    }
    // PRIVATE TREES
    // -------------
    findPrivateNode(path) {
        return findPrivateNode(this.privateNodes, path);
    }
    async addPrivateLogEntry(depot, cid) {
        const log = [...this.privateLog];
        let idx = Math.max(0, log.length - 1);
        // get last chunk
        let lastChunk = log[idx]?.cid
            ? (await depot
                .getUnixFile(decodeCID(log[idx].cid))
                .then(a => Uint8arrays.toString(a, "utf8"))
                .then(a => a.split(",")))
            : [];
        // needs new chunk
        const needsNewChunk = lastChunk.length + 1 > RootTree.LOG_CHUNK_SIZE;
        if (needsNewChunk) {
            idx = idx + 1;
            lastChunk = [];
        }
        // add to chunk
        const hashedCid = await this.dependencies.crypto.hash.sha256(Uint8arrays.fromString(encodeCID(cid), "utf8"));
        const updatedChunk = [...lastChunk, hashedCid];
        const updatedChunkDeposit = await protocol.basic.putFile(this.dependencies.depot, Uint8arrays.fromString(updatedChunk.join(","), "utf8"));
        log[idx] = {
            name: idx.toString(),
            cid: updatedChunkDeposit.cid,
            size: updatedChunkDeposit.size
        };
        // save log
        const logCID = await DAG.putPB(this.dependencies.depot, log.map(Link.toDAGLink));
        this.updateLink(Branch.PrivateLog, {
            cid: logCID,
            isFile: false,
            size: await this.dependencies.depot.size(logCID)
        });
        this.privateLog = log;
    }
    // SHARING
    // -------
    async addShares(links) {
        this.sharedLinks = links.reduce((acc, link) => ({ ...acc, [link.name]: link }), this.sharedLinks);
        const cborApprovedLinks = Object.values(this.sharedLinks).reduce((acc, { cid, name, size }) => ({
            ...acc,
            [name]: { cid, name, size }
        }), {});
        const cid = await this.dependencies.depot.putBlock(DagCBOR.encode(cborApprovedLinks), DagCBOR.code);
        this.updateLink(Branch.Shared, {
            cid: cid,
            isFile: false,
            size: await this.dependencies.depot.size(cid)
        });
        return this;
    }
    static async getSharedLinks(depot, cid) {
        const block = await depot.getBlock(cid);
        const decodedBlock = DagCBOR.decode(block);
        if (!TypeChecks.isObject(decodedBlock))
            throw new Error("Invalid shared section, not an object");
        return Object.values(decodedBlock).reduce((acc, link) => {
            if (!TypeChecks.isObject(link))
                return acc;
            const name = link.name ? link.name : null;
            const cid = link.cid
                ? decodeCID(link.cid)
                : null;
            if (!name || !cid)
                return acc;
            return { ...acc, [name]: { name, cid, size: (link.size || 0) } };
        }, {});
    }
    async setSharedCounter(counter) {
        this.sharedCounter = counter;
        const { cid, size } = await protocol.basic.putFile(this.dependencies.depot, Uint8arrays.fromString(JSON.stringify(counter), "utf8"));
        this.updateLink(Branch.SharedCounter, {
            cid: cid,
            isFile: true,
            size: size
        });
        return counter;
    }
    async bumpSharedCounter() {
        const newCounter = this.sharedCounter + 1;
        return this.setSharedCounter(newCounter);
    }
    // VERSION
    // -------
    async setVersion(v) {
        const version = Uint8arrays.fromString(Versions.toString(v), "utf8");
        const result = await protocol.basic.putFile(this.dependencies.depot, version);
        return this.updateLink(Branch.Version, result);
    }
    async getVersion() {
        return await parseVersionFromLinks(this.dependencies.depot, this.links);
    }
}
// PRIVATE LOG
// -----------
// CBOR array containing chunks.
//
// Chunk size is based on the default IPFS block size,
// which is 1024 * 256 bytes. 1 log chunk should fit in 1 block.
// We'll use the CSV format for the data in the chunks.
RootTree.LOG_CHUNK_SIZE = 1020; // Math.floor((1024 * 256) / (256 + 1))
async function findBareNameFilter(crypto, storage, accountDID, map, path) {
    const bareNameFilterId = await Identifiers.bareNameFilter({ accountDID, crypto, path });
    const bareNameFilter = await storage.getItem(bareNameFilterId);
    if (bareNameFilter)
        return bareNameFilter;
    const [nodePath, node] = findPrivateNode(map, path);
    if (!node)
        return null;
    const unwrappedPath = Path.unwrap(path);
    const relativePath = unwrappedPath.slice(Path.unwrap(nodePath).length);
    if (PrivateFile.instanceOf(node)) {
        return relativePath.length === 0 ? node.header.bareNameFilter : null;
    }
    if (!node.exists(relativePath)) {
        if (Path.isDirectory(path))
            await node.mkdir(relativePath);
        else
            await node.add(relativePath, new Uint8Array());
    }
    return node.get(relativePath).then(t => t ? t.header.bareNameFilter : null);
}
function findPrivateNode(map, path) {
    const t = map[Path.toPosix(path)];
    if (t)
        return [path, t];
    const parent = Path.parent(path);
    return parent
        ? findPrivateNode(map, parent)
        : [path, null];
}
function loadPrivateNodes(dependencies, accountDID, pathKeys, mmpt) {
    const { crypto, storage } = dependencies;
    return sortedPathKeys(pathKeys).reduce((acc, { path, key }) => {
        return acc.then(async (map) => {
            let privateNode;
            const unwrappedPath = Path.unwrap(path);
            // if root, no need for bare name filter
            if (unwrappedPath.length === 1 && unwrappedPath[0] === Path.Branch.Private) {
                privateNode = await PrivateTree.fromBaseKey(dependencies.crypto, dependencies.depot, dependencies.manners, dependencies.reference, mmpt, key);
            }
            else {
                const bareNameFilter = await findBareNameFilter(crypto, storage, accountDID, map, path);
                if (!bareNameFilter)
                    throw new Error(`Was trying to load the PrivateTree for the path \`${path}\`, but couldn't find the bare name filter for it.`);
                if (Path.isDirectory(path)) {
                    privateNode = await PrivateTree.fromBareNameFilter(dependencies.crypto, dependencies.depot, dependencies.manners, dependencies.reference, mmpt, bareNameFilter, key);
                }
                else {
                    privateNode = await PrivateFile.fromBareNameFilter(dependencies.crypto, dependencies.depot, mmpt, bareNameFilter, key);
                }
            }
            const posixPath = Path.toPosix(path);
            return { ...map, [posixPath]: privateNode };
        });
    }, Promise.resolve({}));
}
async function parseVersionFromLinks(depot, links) {
    const file = await protocol.basic.getFile(depot, decodeCID(links[Branch.Version].cid));
    return Versions.fromString(Uint8arrays.toString(file)) ?? Versions.v0;
}
async function permissionKeys(crypto, accountDID, permissions) {
    return permissionPaths(permissions).reduce(async (acc, path) => {
        if (Path.isBranch(Path.Branch.Public, path))
            return acc;
        const name = await Identifiers.readKey({ accountDID, crypto, path });
        const key = await crypto.keystore.exportSymmKey(name);
        const pk = { path: path, key: key };
        return acc.then(list => [...list, pk]);
    }, Promise.resolve([]));
}
/**
 * Sort keys alphabetically by path.
 * This is used to sort paths by parent first.
 */
function sortedPathKeys(list) {
    return list.sort((a, b) => Path.toPosix(a.path).localeCompare(Path.toPosix(b.path)));
}
//# sourceMappingURL=tree.js.map