import { assert } from "../assert.js";
import { string_repeat } from "../primitives/strings/string_repeat.js";

class Line {
    /**
     *
     * @param {string} text
     * @param {number} indent
     */
    constructor(text, indent) {
        assert.isString(text, 'text');
        assert.isNonNegativeInteger(indent, 'index');

        /**
         *
         * @type {string}
         */
        this.text = text;
        /**
         *
         * @type {number}
         */
        this.indentation = indent;
    }

    toString() {
        return `Line{ indentation=${this.indentation}, text="${this.text}" }`
    }
}

const DEFAULT_INDENT_SPACES = 4;

/**
 * Useful for generating formatted snippets of code.
 *
 * @example
 *  const b = new LineBuilder();
 *  b.add("function hello(){");
 *  b.indent();
 *  b.add("return \"hello world\";");
 *  b.dedent();
 *  b.add("}");
 *  b.build();
 *  // function hello(){
 *  //     return "hello world";
 *  // }
 */
class LineBuilder {

    /**
     *
     * @type {Line[]}
     */
    #lines = [];

    /**
     * Current indent level
     * @type {number}
     */
    #indentation = 0;

    /**
     * Current indentation level
     * Mainly intended for testing
     * @return {number}
     */
    get indentation() {
        return this.#indentation;
    }

    /**
     * TODO replace with indent string, that is tab, space or any combination or something else entirely
     * @private
     * @type {number}
     */
    indentSpaces = DEFAULT_INDENT_SPACES;

    /**
     * Number of lines
     * @return {number}
     */
    get count() {
        return this.#lines.length;
    }

    /**
     * Substring test on a per-line basis.
     * A match can only span a single line, so multi-line matches will not be found.
     * @param {string} term
     * @return {boolean} true if the term was found at least once, false otherwise
     */
    containsSubstring(term) {
        assert.isString(term, 'term');

        const lines = this.#lines;
        const n = lines.length;

        for (let i = 0; i < n; i++) {
            const line = lines[i];

            const termIndex = line.text.indexOf(term);

            if (termIndex !== -1) {
                // match
                return true;
            }
        }

        return false;
    }

    /**
     *
     * @returns {LineBuilder}
     */
    indent() {
        this.#indentation++;
        return this;
    }

    /**
     * NOTE: clamps indentation to 0 (can't go negative)
     * @returns {LineBuilder}
     */
    dedent() {

        // clamp to 0
        this.#indentation = Math.max(0, this.#indentation - 1);

        return this;
    }

    /**
     * Adds an entire new line with a given text.
     * Inherits the current indentation level.
     * @param {string} line_text
     * @returns {LineBuilder}
     */
    add(line_text) {

        const line = new Line(line_text, this.#indentation);

        this.#lines.push(line);

        return this;
    }

    /**
     * Appends text to the last line.
     *
     * @param {string} text
     * @returns {void}
     * @example
     *  const b = new LineBuilder();
     *  b.add("hello");
     *  b.extend(" world");
     *  b.build(); // "hello world"
     *
     */
    extend(text) {
        assert.isString(text, 'text');

        const lines = this.#lines;
        const last_index = lines.length - 1;

        if (last_index < 0) {
            throw new Error("No lines to append to");
        }

        lines[last_index].text += text;
    }

    /**
     *
     * @param {LineBuilder} lines
     */
    addLines(lines) {
        assert.defined(lines, 'lines');

        const other_lines = lines.#lines;

        const other_line_count = other_lines.length;

        for (let i = 0; i < other_line_count; i++) {
            const otherLine = other_lines[i];

            const line = new Line(otherLine.text, otherLine.indentation + this.#indentation);

            this.#lines.push(line);
        }

    }

    clear() {
        this.#lines = [];
        this.#indentation = 0;
    }

    /**
     * Renders out an indented string of lines
     * @returns {string}
     */
    build() {
        const result = [];

        const lines = this.#lines;
        const line_count = lines.length;

        const indent_string = string_repeat(' ', this.indentSpaces);

        for (let i = 0; i < line_count; i++) {
            const line = lines[i];

            let prefix = '';

            const indent_count = line.indentation;

            for (let j = 0; j < indent_count; j++) {
                prefix += indent_string;
            }

            result.push(prefix + line.text);
        }

        return result.join('\n');
    }

    /**
     * @param {string} text
     * @param {string} [line_separator] defaults to new-line character
     * @returns {LineBuilder}
     */
    static fromText(text, line_separator = '\n') {
        assert.isString(text, 'text');
        assert.isString(line_separator, 'line_separator');

        const r = new LineBuilder();

        const lines = text.split(line_separator);

        const n = lines.length;

        // TODO detect indent

        for (let i = 0; i < n; i++) {

            const line_text = lines[i];

            r.add(line_text);

        }

        return r;
    }

    toString() {
        return this.build();
    }
}

export default LineBuilder;
