/**
 * General set of functions to programmatically interact with HEdit.
 * @module hedit
 */

// All the builtin modules are evaluated in a context where there's a global `__hedit`
// which acts as a bridge between the JS and the C worlds.

import EventEmitter from 'hedit/private/eventemitter';

function parseHex(str) {
    const m = str.match(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
    if (!m) {
        throw new Error(`Invalid color: ${str}.`);
    }

    let r = parseInt(m[1], 16);
    let g = parseInt(m[2], 16);
    let b = parseInt(m[3], 16);

    // Grays live in another space
    if (r == g && g == b) {
        return 232 + Math.floor(r / 255 * 23);
    } else {
        r = Math.floor(r / 255 * 5);
        g = Math.floor(g / 255 * 5);
        b = Math.floor(b / 255 * 5);
        return 16 + 36 * r + 6 * g + b;
    }
}

function parseColor(c) {
    if (typeof c === 'string') {
        return parseHex(c);
    } else if (typeof c === 'number') {
        const n = Math.floor(c);
        if (n < 0 || n > 255) {
            throw new Error(`Invalid integer color value ${n}.`);
        }
        return n;
    } else {
        throw new Error(`Invalid color: ${c}.`);
    }
}

function expandPen(pen, defaults) {
    
    // Missing property
    if (typeof pen === 'undefined' || pen === null) {
        return defaults;

    // Hex or integer color
    } else if (typeof pen === 'string' || typeof pen === 'number') {
        return Object.assign({}, defaults, { fg: parseColor(pen) });

    // Complex object
    } else if (typeof pen === 'object') {
        let clone = Object.assign({}, defaults, pen);
        clone.fg = parseColor(clone.fg);
        clone.bg = parseColor(clone.bg);
        clone.bold = !!clone.bold;
        clone.under = !!clone.under;
        return clone;

    // Invalid
    } else {
        throw new Error(`Invalid pen descriptor: ${pen}`);
    }

}

/*
 * The user can represent themes with a wide variety of shortcuts,
 * but the final form of the descriptor that we have to pass to the native method is as follows:
 *
 * {
 *     text: { fg: [0 - 255], bg: [0 - 255], bold: true|false, under: true|false },
 *     linenos: ...,
 *     error: ...,
 *     ...
 * }
 *
 * All the properties are required, and must have the same exact format.
 *
 * The user can also specify colors in hex, which means that we have to convert them
 * to terminal colors.
 */

function expandTheme(t) {

    // Default theme
    const defaultTheme = {
        text:         { fg: 7,   bg: 16,  bool: false, under: false },
        linenos:      { fg: 8,   bg: 16,  bold: false, under: false },
        error:        { fg: 1,   bg: 16,  bold: true,  under: false },
        block_cursor: { fg: 16,  bg: 7,   bool: false, under: false },
        soft_cursor:  { fg: 7,   bg: 16,  bold: true,  under: true  },
        statusbar:    { fg: 234, bg: 247, bold: false, under: false },
        commandbar:   { fg: 7,   bg: 16,  bool: false, under: false },
        log_debug:    { fg: 8,   bg: 16,  bold: false, under: false },
        log_info:     { fg: 6,   bg: 16,  bold: false, under: false },
        log_warn:     { fg: 3,   bg: 16,  bold: true,  under: false },
        log_error:    { fg: 1,   bg: 16,  bold: true,  under: false },
        log_fatal:    { fg: 5,   bg: 16,  bold: true,  under: false },
        white:        { fg: 7,   bg: 16,  bold: false, under: false },
        gray:         { fg: 8,   bg: 16,  bold: false, under: false },
        blue:         { fg: 4,   bg: 16,  bold: false, under: false },
        red:          { fg: 1,   bg: 16,  bold: false, under: false },
        pink:         { fg: 13,  bg: 16,  bold: false, under: false },
        green:        { fg: 2,   bg: 16,  bold: false, under: false },
        purple:       { fg: 5,   bg: 16,  bold: false, under: false },
        orange:       { fg: 208, bg: 16,  bold: false, under: false }
    };

    let penDescriptor = {};
    const textprops = [ 'text', 'linenos', 'error', 'block_cursor', 'soft_cursor', 'commandbar', 'statusbar', 'log_debug', 'log_info', 'log_warn', 'log_error', 'log_fatal', 'white', 'gray', 'blue', 'red', 'pink', 'green', 'purple', 'orange' ];
    for (let k of textprops) {
        penDescriptor[k] = expandPen(t[k], defaultTheme[k]);
    }

    return penDescriptor;
}

class HEdit extends EventEmitter {

    /**
     * The name of the mode the editor is currently in.
     * @alias module:hedit.mode
     * @type {string}
     * @readonly
     */
    get mode() {
        return __hedit.mode();
    }

    /**
     * The name of the currently active view.
     * @alias module:hedit.view
     * @type {string}
     * @readonly
     */
    get view() {
        return __hedit.view();
    }

    /**
     * Sets the current theme.
     *
     * A theme determines the colors used by the editor to render itself.
     * Different parts of the UI can be drawn with different pens, which contains the actual
     * style information. For example, to draw the line offsets bold green and text blue, we can use:
     *
     * ```
     * {
     *     linenos: { fg: '00ff00', bold: true },
     *     text: '0000ff' // Shortcut for `{ fg: '0000ff' }`
     * }
     * ```
     *
     * The following parts of the UI can be themed:
     * - `text`
     * - `linenos`
     * - `error`
     * - `block_cursor`
     * - `soft_cursor`
     * - `statusbar`
     * - `commandbar`
     * - `log_debug`
     * - `log_info`
     * - `log_warn`
     * - `log_error`
     * - `log_fatal`
     *
     * Each of these parts can be themed with a pen, which has 4 properties:
     * - `fg`
     * - `bg`
     * - `bold`
     * - `under`
     *
     * Colors can be specified either using standard ANSI number or hex RGB values,
     * which will be rounded to the closest color supported by the terminal.
     *
     * If any of the previous properties is missing, the default value will be used.
     *
     * For an example of the usage of `setTheme`, see {@tutorial colorful-statusbar}.
     *
     * @alias module:hedit.setTheme
     * @param {object} t - Theme description.
     * @throws Throws if an invalid theme description is passed.
     * @see Usage example: {@tutorial colorful-statusbar}.
     */
    setTheme(t) {
        __hedit.setTheme(expandTheme(t));
    }

    /**
     * Emits the given keys as if the user actually typed them.
     *
     * @alias module:hedit.emitKeys
     * @param {string} keys - Keys to emit.
     *
     * @example
     * hedit.emitKeys('<Escape>:w<Enter>i');
     */
    emitKeys(keys) {
        __hedit.emitKeys(keys);
    }

    /**
     * Executes a command.
     * @alias module:hedit.command
     * @param {string} cmd - Command to execute.
     * @return {boolean} Returns `true` if the command executed successfully, `false` otherwise.
     *
     * @example
     * hedit.command('q!'); // Exits the editor
     */
    command(cmd) {
        return __hedit.command(cmd);
    }

    /**
     * Handler of a custom command.
     * @callback CommandCallback
     * @param {...string} args - All the commands supplied by the user at the moment
              of the invocation of the command.
     */

    /**
     * Registers a new command, whose implementation is up to the user.
     * @alias module:hedit.registerCommand
     * @param {string} name - Command name.
     * @param {CommandCallback} handler - Function implementing the command.
     * @throws Throws if the command registration fails.
     *
     * @example
     * hedit.registerCommand('special', n => {
     *     log.info('The argument is', parseInt(n, 10) % 2 == 0 ? 'even' : 'odd');
     * });
     */
    registerCommand(name, handler) {
        if (typeof handler !== 'function') {
            throw new Error('Handler must be a function.');
        }
        if (!__hedit.registerCommand(name, handler)) {
            throw new Error('Command registration failed.');
        }
    }

    /**
     * Handler of a custom option.
     * @callback OptionCallback
     * @param {string} newValue - New value of the option.
     * @return {boolean} Returns `true` if the change is accepted, `false` otherwise.
     */

    /**
     * Registers a new option, whose implementation is up to the user.
     * @alias module:hedit.registerOption
     * @param {string} name - Name of the option.
     * @param {string} defaultValue - Default option value.
     * @param {OptionCallback} handler - Function to be called when the value of the option changes.
     * @thorws Throws if the option registration fails.
     *
     * @example
     * hedit.registerOption('cool', false, newValue => {
     *     if (newValue === 'true') {
     *         log.info('The coolness is now on!');
     *         return true;
     *     } else if (newValue === 'false') {
     *         log.info('So sad :(');
     *         return true;
     *     } else {
     *         // Invalid value
     *         return false;
     *     }
     * });
     */
    registerOption(name, defaultValue, handler) {
        if (typeof handler !== 'function') {
            throw new Error('Handler must be a function.');
        }
        if (!__hedit.registerOption(name, defaultValue, handler)) {
            throw new Error('Option registration failed.');
        }
    }

    /**
     * Registers a new key mapping.
     * @alias module:hedit.map
     * @param {string} mode - Mode the new mapping is valid in.
     * @param {string} from - Key to map.
     * @param {string} to - The key sequence that the `from` key will be expanded to.
     * @param {boolean} [force = false] - Skip the check for an existing mapping for the same key.
     * @throws Throws if the mapping registration fails.
     * @see [emitKeys]{@link module:hedit.emitKeys} for more information about the format of the keys.
     *
     * @example
     * hedit.map('insert', '<C-j>', 'cafebabe');
     */
    map(mode, from, to, force = false) {
        if (!__hedit.map(mode, from, to, !!force)) {
            throw new Error('Key mapping registration failed.');
        }
    }

    /**
     * Sets the value of an option.
     * @alias module:hedit.set
     * @param {string} name - Option name.
     * @param {string|number} value - Value of the option.
     * @throws Throws if there's an error setting the option.
     *
     * @example
     * hedit.set('colwidth', 8);
     */
    set(name, value) {
        if (!__hedit.set(name, value)) {
            throw new Error(`Failed to set option ${name}.`);
        }
    }

    /**
     * Retrives the value of an option.
     * @alias module:hedit.get
     * @param {string} name - Option name.
     * @return {*} The current value of the option.
     * @throws Throws if the option name is invalid.
     *
     * @example
     * log.info('Current colwidth:', hedit.get('colwidth'));
     */
    get(name) {
        return __hedit.get(name);
    }

    /**
     * Switches the editor to the given mode.
     * @alias module:hedit.switchMode
     * @param {string} name - Name of the mode to switch to.
     *
     * @example
     * hedit.switchMode('insert');
     */
    switchMode(name) {
        __hedit.switchMode(name);
    }

};

const hedit = new HEdit();

__hedit.registerEventBroker((name, ...args) => {
    hedit.emit(name.substring(6) /* Chop off `hedit/` from the name. */, ...args);
});

/**
 * Event raised only once when the editor is fully loaded and ready.
 * @event load
 */

/**
 * Event raised only once when the editor is going to close.
 * @event quit
 */

/**
 * Event raised when the user switches mode.
 * @event mode-switch
 * @param {string} mode - New mode set.
 */

/**
 * Event raised when the user switches view.
 * @event view-switch
 * @param {string} view - New active view.
 */
 
/**
 * Event raised when a new file is opened by the user.
 * @event file/open
 */

/**
 * Event raised when the file has been successfully written to disk.
 * @event file/write
 */
 
/**
 * Event raised when the open file is closed.
 * @event file/close
 */
 
/**
 * Event raised when the contents of the file change.
 * @event file/change
 * @param {integer} offset Offset of the change.
 * @param {integer} len Length of the change.
 */

export default hedit;