/*
 * 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 {ServerFormatting} = require('./server-formatting');
const {PreparedStatementError} = require('../errors');
const {QueryFile} = require('../query-file');
const {assert} = require('../assert');

const npm = {
    EOL: require('os').EOL,
    utils: require('../utils')
};

/**
 * @class PreparedStatement
 * @description
 * Constructs a new $[Prepared Statement] object. All properties can also be set after the object's construction.
 *
 * This type extends the basic `{name, text, values}` object, i.e. when the basic object is used
 * with a query method, a new {@link PreparedStatement} object is created in its place.
 *
 * The type can be used in place of the `query` parameter, with any query method directly.
 *
 * The type is available from the library's root: `pgp.PreparedStatement`.
 *
 * @param {Object} [options]
 * Object configuration options / properties.
 *
 * @param {string} [options.name] - See property {@link PreparedStatement#name name}.
 * @param {string|QueryFile} [options.text] - See property {@link PreparedStatement#text text}.
 * @param {array} [options.values] - See property {@link PreparedStatement#values values}.
 * @param {boolean} [options.binary] - See property {@link PreparedStatement#binary binary}.
 * @param {string} [options.rowMode] - See property {@link PreparedStatement#rowMode rowMode}.
 * @param {number} [options.rows] - See property {@link PreparedStatement#rows rows}.
 * @param {ITypes} [options.types] - See property {@link PreparedStatement#types types}.
 *
 * @returns {PreparedStatement}
 *
 * @see
 * {@link errors.PreparedStatementError PreparedStatementError},
 * {@link http://www.postgresql.org/docs/9.6/static/sql-prepare.html PostgreSQL Prepared Statements}
 *
 * @example
 *
 * const {PreparedStatement: PS} = require('pg-promise');
 *
 * // Creating a complete Prepared Statement with parameters:
 * const findUser = new PS({name: 'find-user', text: 'SELECT * FROM Users WHERE id = $1', values: [123]});
 *
 * db.one(findUser)
 *     .then(user => {
 *         // user found;
 *     })
 *     .catch(error => {
 *         // error;
 *     });
 *
 * @example
 *
 * const {PreparedStatement: PS} = require('pg-promise');
 *
 * // Creating a reusable Prepared Statement without values:
 * const addUser = new PS({name: 'add-user', text: 'INSERT INTO Users(name, age) VALUES($1, $2)'});
 *
 * // setting values explicitly:
 * addUser.values = ['John', 30];
 *
 * db.none(addUser)
 *     .then(() => {
 *         // user added;
 *     })
 *     .catch(error => {
 *         // error;
 *     });
 *
 * // setting values implicitly, by passing them into the query method:
 * db.none(addUser, ['Mike', 25])
 *     .then(() => {
 *         // user added;
 *     })
 *     .catch(error => {
 *         // error;
 *     });
 */
class PreparedStatement extends ServerFormatting {
    constructor(options) {
        options = assert(options, ['name', 'text', 'values', 'binary', 'rowMode', 'rows', 'types']);
        super(options);
    }

    /**
     * @name PreparedStatement#name
     * @type {string}
     * @description
     * An arbitrary name given to this particular prepared statement. It must be unique within a single session and is
     * subsequently used to execute or deallocate a previously prepared statement.
     */
    get name() {
        return this._inner.options.name;
    }

    set name(value) {
        const _i = this._inner;
        if (value !== _i.options.name) {
            _i.options.name = value;
            _i.changed = true;
        }
    }

    /**
     * @name PreparedStatement#rows
     * @type {number}
     * @description
     * Number of rows to return at a time from a Prepared Statement's portal.
     * The default is 0, which means that all rows must be returned at once.
     */
    get rows() {
        return this._inner.options.rows;
    }

    set rows(value) {
        const _i = this._inner;
        if (value !== _i.options.rows) {
            _i.options.rows = value;
            _i.changed = true;
        }
    }
}

/**
 * @method PreparedStatement#parse
 * @description
 * Parses the current object and returns a simple `{name, text, values}`, if successful,
 * or else it returns a {@link errors.PreparedStatementError PreparedStatementError} object.
 *
 * This method is primarily for internal use by the library.
 *
 * @returns {{name, text, values}|errors.PreparedStatementError}
 */
PreparedStatement.prototype.parse = function () {

    const _i = this._inner, options = _i.options;

    const qf = options.text instanceof QueryFile ? options.text : null;

    if (!_i.changed && !qf) {
        return _i.target;
    }

    const errors = [], values = _i.target.values;
    _i.target = {
        name: options.name,
        text: options.text
    };
    _i.changed = true;
    _i.currentError = undefined;

    if (!npm.utils.isText(_i.target.name)) {
        errors.push('Property \'name\' must be a non-empty text string.');
    }

    if (qf) {
        qf.prepare();
        if (qf.error) {
            errors.push(qf.error);
        } else {
            _i.target.text = qf[QueryFile.$query];
        }
    }
    if (!npm.utils.isText(_i.target.text)) {
        errors.push('Property \'text\' must be a non-empty text string.');
    }

    if (!npm.utils.isNull(values)) {
        _i.target.values = values;
    }

    if (options.binary !== undefined) {
        _i.target.binary = !!options.binary;
    }

    if (options.rowMode !== undefined) {
        _i.target.rowMode = options.rowMode;
    }

    if (options.rows !== undefined) {
        _i.target.rows = options.rows;
    }

    if (options.types !== undefined) {
        _i.target.types = options.types;
    }

    if (errors.length) {
        return _i.currentError = new PreparedStatementError(errors[0], _i.target);
    }

    _i.changed = false;

    return _i.target;
};

/**
 * @method PreparedStatement#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}
 */
PreparedStatement.prototype.toString = function (level) {
    level = level > 0 ? parseInt(level) : 0;
    const gap = npm.utils.messageGap(level + 1);
    const ps = this.parse();
    const lines = [
        'PreparedStatement {',
        gap + 'name: ' + npm.utils.toJson(this.name)
    ];
    if (npm.utils.isText(ps.text)) {
        lines.push(gap + 'text: "' + ps.text + '"');
    }
    if (this.values !== undefined) {
        lines.push(gap + 'values: ' + npm.utils.toJson(this.values));
    }
    if (this.binary !== undefined) {
        lines.push(gap + 'binary: ' + npm.utils.toJson(this.binary));
    }
    if (this.rowMode !== undefined) {
        lines.push(gap + 'rowMode: ' + npm.utils.toJson(this.rowMode));
    }
    if (this.rows !== undefined) {
        lines.push(gap + 'rows: ' + npm.utils.toJson(this.rows));
    }
    if (this.error) {
        lines.push(gap + 'error: ' + this.error.toString(level + 1));
    }
    lines.push(npm.utils.messageGap(level) + '}');
    return lines.join(npm.EOL);
};

module.exports = {PreparedStatement};

/**
 * @name PreparedStatement#text
 * @type {string|QueryFile}
 * @description
 * A non-empty query string or a {@link QueryFile} object.
 *
 * Only the basic variables (`$1`, `$2`, etc) can be used in the query, because $[Prepared Statements]
 * are formatted on the server side.
 *
 * Changing this property for the same {@link PreparedStatement#name name} will have no effect, because queries
 * for Prepared Statements are cached by the server, with {@link PreparedStatement#name name} being the cache key.
 */

/**
 * @name PreparedStatement#values
 * @type {array}
 * @description
 * Query formatting parameters, depending on the type:
 *
 * - `null` / `undefined` means the query has no formatting parameters
 * - `Array` - it is an array of formatting parameters
 * - None of the above, means it is a single formatting value, which
 *   is then automatically wrapped into an array
 */

/**
 * @name PreparedStatement#binary
 * @type {boolean}
 * @default undefined
 * @description
 * Activates binary result mode. The default is the text mode.
 *
 * @see {@link http://www.postgresql.org/docs/devel/static/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY Extended Query}
 */

/**
 * @name PreparedStatement#rowMode
 * @type {string}
 * @default undefined
 * @description
 * Changes the way data arrives to the client, with only one value supported by $[pg]:
 *  - `array` will make all data rows arrive as arrays of values. By default, rows arrive as objects.
 */

/**
 * @name PreparedStatement#types
 * @type {ITypes}
 * @default undefined
 * @description
 * Custom type parsers just for this query result.
 */

/**
 * @name PreparedStatement#error
 * @type {errors.PreparedStatementError}
 * @default undefined
 * @description
 * When in an error state, it is set to a {@link errors.PreparedStatementError PreparedStatementError} object. Otherwise, it is `undefined`.
 *
 * This property is primarily for internal use by the library.
 */