/*
 * Copyright (C) 2024-present Puter Technologies Inc.
 *
 * This file is part of Puter.
 *
 * Puter is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
const _path = require('path');
const { PuterPath } = require('../lib/PuterPath');

/**
 * The base class doesn't add any functionality, but it's useful for
 * `instanceof` checks.
 */
class NodeSelector {
    constructor () {
        if ( this.constructor === NodeSelector ) {
            throw new Error('cannot instantiate NodeSelector directly; ' +
                'that would be like using this: https://devmeme.puter.site/plug.webp');
        }
    }
}

class NodePathSelector extends NodeSelector {
    constructor (path) {
        super();
        this.value = path;
    }

    setPropertiesKnownBySelector (node) {
        node.path = this.value;
        node.name = _path.basename(this.value);
    }

    describe () {
        return this.value;
    }
}

class NodeUIDSelector extends NodeSelector {
    constructor (uid) {
        super();
        this.value = uid;
    }

    setPropertiesKnownBySelector (node) {
        node.uid = this.value;
    }

    // Note: the selector could've been added by FSNodeContext
    // during fetch, but this was more efficient because the
    // object is created lazily, and it's somtimes not needed.
    static implyFromFetchedData (node) {
        if ( node.uid ) {
            return new NodeUIDSelector(node.uid);
        }
        return null;
    }

    describe () {
        return `[uid:${this.value}]`;
    }
}

class NodeInternalIDSelector extends NodeSelector {
    constructor (service, id, debugInfo) {
        super();
        this.service = service;
        this.id = id;
        this.debugInfo = debugInfo;
    }

    setPropertiesKnownBySelector (node) {
        if ( this.service === 'mysql' ) {
            node.mysql_id = this.id;
        }
    }

    describe (showDebug) {
        if ( showDebug ) {
            return `[db:${this.id}] (${
                JSON.stringify(this.debugInfo, null, 2)
            })`;
        }
        return `[db:${this.id}]`;
    }
}

class NodeChildSelector extends NodeSelector {
    constructor (parent, name) {
        super();
        this.parent = parent;
        this.name = name;
    }

    setPropertiesKnownBySelector (node) {
        node.name = this.name;

        try_infer_attributes(this);
        if ( this.path ) {
            node.path = this.path;
        }
    }

    describe () {
        return `${this.parent.describe() }/${ this.name}`;
    }
}

class RootNodeSelector extends NodeSelector {
    static entry = {
        is_dir: true,
        is_root: true,
        uuid: PuterPath.NULL_UUID,
        name: '/',
    };
    setPropertiesKnownBySelector (node) {
        node.path = '/';
        node.root = true;
        node.uid = PuterPath.NULL_UUID;
    }
    constructor () {
        super();
        this.entry = this.constructor.entry;
    }

    describe () {
        return '[root]';
    }
}

class NodeRawEntrySelector extends NodeSelector {
    constructor (entry) {
        super();
        // Fix entries from get_descendants
        if ( !entry.uuid && entry.uid ) {
            entry.uuid = entry.uid;
            if ( entry._id ) {
                entry.id = entry._id;
                delete entry._id;
            }
        }

        this.entry = entry;
    }

    setPropertiesKnownBySelector (node) {
        node.found = true;
        node.entry = this.entry;
        node.uid = this.entry.uid ?? this.entry.uuid;
        node.name = this.entry.name;
        if ( this.entry.path ) node.path = this.entry.path;
    }

    describe () {
        return '[raw entry]';
    }
}

/**
 * Try to infer following attributes for a selector:
 * - path
 * - uid
 *
 * @param {NodePathSelector | NodeUIDSelector | NodeChildSelector | RootNodeSelector | NodeRawEntrySelector} selector
 */
function try_infer_attributes (selector) {
    if ( selector instanceof NodePathSelector ) {
        selector.path = selector.value;
    } else if ( selector instanceof NodeUIDSelector ) {
        selector.uid = selector.value;
    } else if ( selector instanceof NodeChildSelector ) {
        try_infer_attributes(selector.parent);
        if ( selector.parent.path ) {
            selector.path = _path.join(selector.parent.path, selector.name);
        }
    } else if ( selector instanceof RootNodeSelector ) {
        selector.path = '/';
    } else {
        // give up
    }
}

const relativeSelector = (parent, path) => {
    if ( path === '.' ) return parent;
    if ( path.startsWith('..') ) {
        throw new Error('currently unsupported');
    }

    let selector = parent;

    const parts = path.split('/').filter(Boolean);
    for ( const part of parts ) {
        selector = new NodeChildSelector(selector, part);
    }

    return selector;
};

module.exports = {
    NodeSelector,
    NodePathSelector,
    NodeUIDSelector,
    NodeInternalIDSelector,
    NodeChildSelector,
    RootNodeSelector,
    NodeRawEntrySelector,
    relativeSelector,
    try_infer_attributes,
};
