/*
 * Copyright (c) 2015-present, Vitaly Tomilov
 *
 * See the LICENSE file at the top-level directory of this distribution
 * for licensing information.
 *
 * Removal or modification of this copyright notice is prohibited.
 */

const {InnerState} = require(`./inner-state`);
const {QueryFileError} = require(`./errors`);
const {assert} = require(`./assert`);
const {ColorConsole} = require(`./utils/color`);

const npm = {
    fs: require(`fs`),
    os: require(`os`),
    path: require(`path`),
    minify: require(`pg-minify`),
    utils: require(`./utils`),
    formatting: require(`./formatting`)
};

const file$query = Symbol(`QueryFile.query`);

/**
 * @class QueryFile
 * @description
 *
 * Represents an external SQL file. The type is available from the library's root: `pgp.QueryFile`.
 *
 * Reads a file with SQL and prepares it for execution, also parses and minifies it, if required.
 * The SQL can be of any complexity, with both single and multi-line comments.
 *
 * The type can be used in place of the `query` parameter, with any query method directly, plus as `text` in {@link PreparedStatement}
 * and {@link ParameterizedQuery}.
 *
 * It never throws any error, leaving it for query methods to reject with {@link errors.QueryFileError QueryFileError}.
 *
 * **IMPORTANT:** You should only create a single reusable object per file, in order to avoid repeated file reads,
 * as the IO is a very expensive resource. If you do not follow it, you will be seeing the following warning:
 * `Creating a duplicate QueryFile object for the same file`, which signals a bad-use pattern.
 *
 * @param {string} file
 * Path to the SQL file with the query, either absolute or relative to the application's entry point file.
 *
 * If there is any problem reading the file, it will be reported when executing the query.
 *
 * @param {QueryFile.Options} [options]
 * Set of configuration options, as documented by {@link QueryFile.Options}.
 *
 * @returns {QueryFile}
 *
 * @see
 * {@link errors.QueryFileError QueryFileError},
 * {@link QueryFile#toPostgres toPostgres}
 *
 * @example
 * // File sql.js
 *
 * // Proper way to organize an sql provider:
 * //
 * // - have all sql files for Users in ./sql/users
 * // - have all sql files for Products in ./sql/products
 * // - have your sql provider module as ./sql/index.js
 *
 * const {QueryFile} = require('pg-promise');
 * const {join: joinPath} = require('path');
 *
 * // Helper for linking to external query files:
 * function sql(file) {
 *     const fullPath = joinPath(__dirname, file); // generating full path;
 *     return new QueryFile(fullPath, {minify: true});
 * }
 *
 * module.exports = {
 *     // external queries for Users:
 *     users: {
 *         add: sql('users/create.sql'),
 *         search: sql('users/search.sql'),
 *         report: sql('users/report.sql'),
 *     },
 *     // external queries for Products:
 *     products: {
 *         add: sql('products/add.sql'),
 *         quote: sql('products/quote.sql'),
 *         search: sql('products/search.sql'),
 *     }
 * };
 *
 * @example
 * // Testing our SQL provider
 *
 * const db = require('./db'); // our database module;
 * const {users: sql} = require('./sql'); // sql for users;
 *
 * module.exports = {
 *     addUser: (name, age) => db.none(sql.add, [name, age]),
 *     findUser: name => db.any(sql.search, name)
 * };
 *
 */
class QueryFile extends InnerState {

    constructor(file, options) {

        let filePath = file;

        options = assert(options, {
            debug: npm.utils.isDev(),
            minify: (options && options.compress && options.minify === undefined) ? true : undefined,
            compress: undefined,
            params: undefined,
            noWarnings: undefined
        });

        if (npm.utils.isText(filePath) && !npm.path.isAbsolute(filePath)) {
            filePath = npm.path.join(npm.utils.startDir, filePath);
        }

        const {usedPath} = QueryFile.instance;

        // istanbul ignore next:
        if (!options.noWarnings) {
            if (filePath in usedPath) {
                usedPath[filePath]++;
                ColorConsole.warn(`WARNING: Creating a duplicate QueryFile object for the same file - \n    ${filePath}\n${npm.utils.getLocalStack(2, 3)}\n`);
            } else {
                usedPath[filePath] = 0;
            }
        }

        const _inner = {
            file,
            filePath,
            options,
            sql: undefined,
            error: undefined,
            ready: undefined,
            modTime: undefined
        };

        super(_inner);

        this.prepare();
    }

    /**
     * Global instance of the file-path repository.
     *
     * @return {{usedPath: {}}}
     */
    static get instance() {
        const s = Symbol.for(`pgPromiseQueryFile`);
        let scope = global[s];
        if (!scope) {
            scope = {
                usedPath: {} // used-path look-up dictionary
            };
            global[s] = scope;
        }
        return scope;
    }

    /**
     * @name QueryFile#Symbol(QueryFile.$query)
     * @type {string}
     * @default undefined
     * @readonly
     * @private
     * @summary Prepared query string.
     * @description
     * When property {@link QueryFile#error error} is set, the query is `undefined`.
     *
     * **IMPORTANT:** This property is for internal use by the library only, never use this
     * property directly from your code.
     */
    get [file$query]() {
        return this._inner.sql;
    }

    /**
     * @name QueryFile#error
     * @type {errors.QueryFileError}
     * @default undefined
     * @readonly
     * @description
     * When in an error state, it is set to a {@link errors.QueryFileError QueryFileError} object. Otherwise, it is `undefined`.
     */
    get error() {
        return this._inner.error;
    }

    /**
     * @name QueryFile#file
     * @type {string}
     * @readonly
     * @description
     * File name that was passed into the constructor.
     *
     * This property is primarily for internal use by the library.
     */
    get file() {
        return this._inner.file;
    }

    /**
     * @name QueryFile#options
     * @type {QueryFile.Options}
     * @readonly
     * @description
     * Set of options, as configured during the object's construction.
     *
     * This property is primarily for internal use by the library.
     */
    get options() {
        return this._inner.options;
    }

    /**
     * @summary Prepares the query for execution.
     * @description
     * If the query hasn't been prepared yet, it will read the file and process the content according
     * to the parameters passed into the constructor.
     *
     * This method is primarily for internal use by the library.
     *
     * @param {boolean} [throwErrors=false]
     * Throw any error encountered.
     */
    prepare(throwErrors) {
        const i = this._inner, options = i.options;
        let lastMod;
        if (options.debug && i.ready) {
            try {
                lastMod = npm.fs.statSync(i.filePath).mtime.getTime();
                // istanbul ignore if;
                if (lastMod === i.modTime) {
                    return;
                }
                i.ready = false;
            } catch (e) {
                i.sql = undefined;
                i.ready = false;
                i.error = e;
                if (throwErrors) {
                    throw i.error;
                }
                return;
            }
        }
        if (i.ready) {
            return;
        }
        try {
            i.sql = npm.fs.readFileSync(i.filePath, `utf8`);
            i.modTime = lastMod || npm.fs.statSync(i.filePath).mtime.getTime();
            if (options.minify && options.minify !== `after`) {
                i.sql = npm.minify(i.sql, {compress: options.compress});
            }
            if (options.params !== undefined) {
                i.sql = npm.formatting.as.format(i.sql, options.params, {partial: true});
            }
            if (options.minify && options.minify === `after`) {
                i.sql = npm.minify(i.sql, {compress: options.compress});
            }
            i.ready = true;
            i.error = undefined;
        } catch (e) {
            i.sql = undefined;
            i.error = new QueryFileError(e, this);
            if (throwErrors) {
                throw i.error;
            }
        }
    }

}

// Hiding the query as a symbol within the type,
// to make it even more difficult to misuse it:
QueryFile.$query = file$query;

/**
 * @method QueryFile#toPostgres
 * @description
 * $[Custom Type Formatting], based on $[Symbolic CTF], i.e. the actual method is available only via {@link external:Symbol Symbol}:
 *
 * ```js
 * const ctf = pgp.as.ctf; // Custom Type Formatting symbols namespace
 * const query = qf[ctf.toPostgres](); // qf = an object of type QueryFile
 * ```
 *
 * This is a raw formatting type (`rawType = true`), i.e. when used as a query-formatting parameter, type `QueryFile` injects SQL as raw text.
 *
 * If you need to support type `QueryFile` outside of query methods, this is the only safe way to get the most current SQL.
 * And you would want to use this method dynamically, as it reloads the SQL automatically, if option `debug` is set.
 * See {@link QueryFile.Options Options}.
 *
 * @param {QueryFile} [self]
 * Optional self-reference, for ES6 arrow functions.
 *
 * @returns {string}
 * SQL string from the file, according to the {@link QueryFile.Options options} specified.
 *
 */
QueryFile.prototype[npm.formatting.as.ctf.toPostgres] = function (self) {
    self = this instanceof QueryFile && this || self;
    self.prepare(true);
    return self[QueryFile.$query];
};

QueryFile.prototype[npm.formatting.as.ctf.rawType] = true; // use as pre-formatted

/**
 * @method QueryFile#toString
 * @description
 * Creates a well-formatted multi-line string that represents the object's current state.
 *
 * It is called automatically when writing the object into the console.
 *
 * @param {number} [level=0]
 * Nested output level, to provide visual offset.
 *
 * @returns {string}
 */
QueryFile.prototype.toString = function (level) {
    level = level > 0 ? parseInt(level) : 0;
    const gap = npm.utils.messageGap(level + 1);
    const lines = [
        `QueryFile {`
    ];
    this.prepare();
    lines.push(gap + `file: "` + this.file + `"`);
    lines.push(gap + `options: ` + npm.utils.toJson(this.options));
    if (this.error) {
        lines.push(gap + `error: ` + this.error.toString(level + 1));
    } else {
        lines.push(gap + `query: "` + this[QueryFile.$query] + `"`);
    }
    lines.push(npm.utils.messageGap(level) + `}`);
    return lines.join(npm.os.EOL);
};

npm.utils.addInspection(QueryFile, function () {
    return this.toString();
});

module.exports = {QueryFile};

/**
 * @typedef QueryFile.Options
 * @description
 * A set of configuration options as passed into the {@link QueryFile} constructor.
 *
 * @property {boolean} debug
 * When in debug mode, the query file is checked for its last modification time on every query request,
 * so if it changes, the file is read afresh.
 *
 * The default for this property is `true` when `NODE_ENV` = `development`,
 * or `false` otherwise.
 *
 * @property {boolean|string} minify=false
 * Parses and minifies the SQL using $[pg-minify]:
 * - `false` - do not use $[pg-minify]
 * - `true` - use $[pg-minify] to parse and minify SQL
 * - `'after'` - use $[pg-minify] after applying static formatting parameters
 *   (option `params`), as opposed to before it (default)
 *
 * If option `compress` is set, then the default for `minify` is `true`.
 *
 * Failure to parse SQL will result in $[SQLParsingError].
 *
 * @property {boolean} compress=false
 * Sets option `compress` as supported by $[pg-minify], to uglify the SQL:
 * - `false` - no compression to be applied, keep minimum spaces for easier read
 * - `true` - remove all unnecessary spaces from SQL
 *
 * This option has no meaning, if `minify` is explicitly set to `false`. However, if `minify` is not
 * specified and `compress` is specified as `true`, then `minify` defaults to `true`.
 *
 * @property {array|object|value} params
 *
 * Static formatting parameters to be applied to the SQL, using the same method {@link formatting.format as.format},
 * but with option `partial` = `true`.
 *
 * Most of the time query formatting is fully dynamic, and applied just before executing the query.
 * In some cases though you may need to pre-format SQL with static values. Examples of it can be a
 * schema name, or a configurable table name.
 *
 * This option makes two-step SQL formatting easy: you can pre-format the SQL initially, and then
 * apply the second-step dynamic formatting when executing the query.
 *
 * @property {boolean} noWarnings=false
 * Suppresses all warnings produced by the class. It is not recommended for general use, only in specific tests
 * that may require it.
 *
 */
