/*
 * 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/>.
 */

// METADATA // {"ai-commented":{"service":"claude"}}
const dedent = require('dedent');
const doctrine = require('doctrine');


/**
* Out class - A utility class for generating formatted text output
* Provides methods for creating headings, line feeds, and text output
*
* ~~with a fluent interface.~~
*  ^ Nope, AI got this wrong but maybe it's a good idea to
*    make this a fluent interface
*
* The constructor returns a bound function that
* maintains the output state and provides access to helper methods.
*/
class Out {
    constructor () {
        this.str = '';
        const fn = this.out.bind(this);
        fn.h = this.h.bind(this);
        fn.lf = this.lf.bind(this);
        fn.text = () => this.str;
        return fn;
    }
    
    h (n, text) {
        this.str += '#'.repeat(n) + ' ' + text + '\n\n';
    }
    

    /**
    * Adds a line feed (newline) to the output string
    * @returns {void}
    */
    lf () { this.str += '\n'; }
    
    /**
     * Append to the string
     * @param {string} str 
     */
    out (str) {
        this.str += str;
    }
}


/**
* Doc class serves as a base class for documentation generation.
* Provides core functionality for parsing and storing documentation comments
* using the doctrine parser. Contains methods for handling JSDoc-style
* comments and maintaining documentation state.
*/
class Doc {
    constructor () {
        this._construct();
    }
    provide_comment (comment) {
        const parsed_comment = doctrine.parse(comment.value, { unwrap: true });
        this.comment = parsed_comment.description;
    }
}


/**
* ModuleDoc class extends Doc to represent documentation for a module.
* Handles module-level documentation including services, libraries, and requirements.
* Provides methods for adding services/libraries and generating markdown documentation.
* Tracks external imports and generates notes about module dependencies.
*/
class ModuleDoc extends Doc {
    /**
    * Initializes the base properties for a ModuleDoc instance
    * Sets up empty arrays for services, requires, and libs collections
    * @private
    */
    _construct () {
        this.services = [];
        this.requires = [];
        this.libs = [];
    }
    

    /**
    * Creates and adds a new service to this module's services array
    * @returns {ServiceDoc} The newly created service document instance
    */
    add_service () {
        const service = new ServiceDoc();
        this.services.push(service);
        return service;
    }
    

    /**
    * Creates and adds a new LibDoc instance to the module's libs array
    * @returns {LibDoc} The newly created LibDoc instance
    */
    add_lib () {
        const lib = new LibDoc();
        this.libs.push(lib);
        return lib;
    }
    

    /**
     * Populates a "notes" array for the module documentation
     * based on findings about imports.
     */
    ready () {
        this.notes = [];
        const rel_requires = this.requires.filter(r => r.startsWith('../'));
        if ( rel_requires.length > 0 ) {
            this.notes.push({
                title: 'Outside Imports',
                desc: dedent(`
                    This module has external relative imports. When these are
                    removed it may become possible to move this module to an
                    extension.
                    
                    **Imports:**
                    ${rel_requires.map(r => {
                        let maybe_aside = '';
                        if ( r.endsWith('BaseService') ) {
                            maybe_aside = ' (use.BaseService)';
                        }
                        return `- \`${r}\`` + maybe_aside;
                    }).join('\n')}
                `)
            });
        }
    }
    
    toMarkdown ({ hl, out } = { hl: 1 }) {
        this.ready();

        out = out ?? new Out();
        
        out.h(hl, this.name);
        
        out(this.comment + '\n\n');
        
        if ( this.services.length > 0 ) {
            out.h(hl + 1, 'Services');
            
            for ( const service of this.services ) {
                service.toMarkdown({ out, hl: hl + 2 });
            }
        }
        
        if ( this.libs.length > 0 ) {
            out.h(hl + 1, 'Libraries');
            
            for ( const lib of this.libs ) {
                lib.toMarkdown({ out, hl: hl + 2 });
            }
        }

        if ( this.notes.length > 0 ) {
            out.h(hl + 1, 'Notes');
            for ( const note of this.notes ) {
                out.h(hl + 2, note.title);
                out(note.desc);
                out.lf();
            }
        }
        

        return out.text();
    }
}


/**
* ServiceDoc class represents documentation for a service module.
* Handles parsing and formatting of service-related documentation including
* listeners, methods, and their associated parameters. Extends the base Doc class
* to provide specialized documentation capabilities for service components.
*/
class ServiceDoc extends Doc {
    /**
    * Represents documentation for a service
    * Handles parsing and storing service documentation including listeners and methods
    * Initializes with empty arrays for listeners and methods
    */
    _construct () {
        this.listeners = [];
        this.methods = [];
    }
    
    provide_comment (comment) {
        const parsed_comment = doctrine.parse(comment.value, { unwrap: true });
        this.comment = parsed_comment.description;
    }
    
    provide_listener (listener) {
        const parsed_comment = doctrine.parse(listener.comment, { unwrap: true });
        
        const params = [];
        for ( const tag of parsed_comment.tags ) {
            if ( tag.title !== 'evtparam' ) continue;
            const name = tag.description.slice(0, tag.description.indexOf(' '));
            const desc = tag.description.slice(tag.description.indexOf(' '));
            params.push({ name, desc })
        }

        this.listeners.push({
            ...listener,
            comment: parsed_comment.description,
            params,
        });
    }
    
    provide_method (method) {
        const parsed_comment = doctrine.parse(method.comment, { unwrap: true });
        
        const params = [];
        for ( const tag of parsed_comment.tags ) {
            if ( tag.title !== 'param' ) continue;
            const name = tag.name;
            const desc = tag.description;
            params.push({ name, desc })
        }

        this.methods.push({
            ...method,
            comment: parsed_comment.description,
            params,
        });
    }
    
    toMarkdown ({ hl, out } = { hl: 1 }) {
        out = out ?? new Out();
        
        out.h(hl, this.name);
        
        out(this.comment + '\n\n');
        
        if ( this.listeners.length > 0 ) {
            out.h(hl + 1, 'Listeners');
            
            for ( const listener of this.listeners ) {
                out.h(hl + 2, '`' + listener.key + '`');
                out (listener.comment + '\n\n');
                
                if ( listener.params.length > 0 ) {
                    out.h(hl + 3, 'Parameters');
                    for ( const param of listener.params ) {
                        out(`- **${param.name}:** ${param.desc}\n`);
                    }
                    out.lf();
                }
            }
        }
        
        if ( this.methods.length > 0 ) {
            out.h(hl + 1, 'Methods');
            
            for ( const method of this.methods ) {
                out.h(hl + 2, '`' + method.key + '`');
                out (method.comment + '\n\n');
                
                if ( method.params.length > 0 ) {
                    out.h(hl + 3, 'Parameters');
                    for ( const param of method.params ) {
                        out(`- **${param.name}:** ${param.desc}\n`);
                    }
                    out.lf();
                }
            }
        }
        
        return out.text();
    }
}


/**
* LibDoc class for documenting library modules
* Handles documentation for library functions including their descriptions,
* parameters, and markdown generation. Extends the base Doc class to provide
* specialized documentation capabilities for library components.
*/
class LibDoc extends Doc {
    /**
    * Represents documentation for a library module
    * 
    * Handles parsing and formatting documentation for library functions.
    * Stores function definitions with their comments, parameters and descriptions.
    * Can output formatted markdown documentation.
    */
    _construct () {
        this.functions = [];
    }
    
    provide_function ({ key, comment, params }) {
        const parsed_comment = doctrine.parse(comment, { unwrap: true });
        
        const parsed_params = [];
        for ( const tag of parsed_comment.tags ) {
            if ( tag.title !== 'param' ) continue;
            const name = tag.name;
            const desc = tag.description;
            parsed_params.push({ name, desc });
        }
        
        this.functions.push({
            key,
            comment: parsed_comment.description,
            params: parsed_params,
        });
    }
    
    toMarkdown ({ hl, out } = { hl: 1 }) {
        out = out ?? new Out();
        
        out.h(hl, this.name);
        
        console.log('functions?', this.functions);
        
        if ( this.functions.length > 0 ) {
            out.h(hl + 1, 'Functions');
            
            for ( const func of this.functions ) {
                out.h(hl + 2, '`' + func.key + '`');
                out(func.comment + '\n\n');
                
                if ( func.params.length > 0 ) {
                    out.h(hl + 3, 'Parameters');
                    for ( const param of func.params ) {
                        out(`- **${param.name}:** ${param.desc}\n`);
                    }
                    out.lf();
                }
            }
        }
        
        return out.text();
    }
}

module.exports = {
    ModuleDoc,
    ServiceDoc,
};
