import { keyCode, dateHelpers, helpers } from 'utils';
import LocalizationService from '../../services/LocalizationService';
import template from './templates/durationEditor.hbs';
import BaseEditorView from './base/BaseEditorView';
import formRepository from '../formRepository';
import iconWrapRemove from './iconsWraps/iconWrapRemove.html';
import iconWrapNumber from './iconsWraps/iconWrapNumber.html';

const focusablePartId = {
    DAYS: 'days',
    HOURS: 'hours',
    MINUTES: 'minutes',
    SECONDS: 'seconds'
};

const createFocusableParts = function(options) {
    const result = [];
    const settings = {};
    settings.daysSettings = _.defaultsPure(options.days, options.allFocusableParts, {
        text: LocalizationService.get('CORE.FORM.EDITORS.DURATION.WORKDURATION.DAYS'),
        maxLength: 4
    });
    settings.hoursSettings = _.defaultsPure(options.hours, options.allFocusableParts, {
        text: LocalizationService.get('CORE.FORM.EDITORS.DURATION.WORKDURATION.HOURS'),
        maxLength: 4
    });
    settings.minutesSettings = _.defaultsPure(options.minutes, options.allFocusableParts, {
        text: LocalizationService.get('CORE.FORM.EDITORS.DURATION.WORKDURATION.MINUTES'),
        maxLength: 4
    });
    settings.secondsSettings = _.defaultsPure(options.seconds, options.allFocusableParts, {
        text: LocalizationService.get('CORE.FORM.EDITORS.DURATION.WORKDURATION.SECONDS'),
        maxLength: 4
    });
    Object.values(settings).forEach(setting => (setting.text = ` ${setting.text}`)); //RegExp in getSegmentValue method based on ' ' (\s)
    if (options.allowDays) {
        result.push({
            id: focusablePartId.DAYS,
            text: settings.daysSettings.text,
            maxLength: settings.daysSettings.maxLength,
            milliseconds: 1000 * 60 * 60 * options.hoursPerDay
        });
    }
    if (options.allowHours) {
        result.push({
            id: focusablePartId.HOURS,
            text: settings.hoursSettings.text,
            maxLength: settings.hoursSettings.maxLength,
            milliseconds: 1000 * 60 * 60
        });
    }
    if (options.allowMinutes) {
        result.push({
            id: focusablePartId.MINUTES,
            text: settings.minutesSettings.text,
            maxLength: settings.minutesSettings.maxLength,
            milliseconds: 1000 * 60
        });
    }
    if (options.allowSeconds) {
        result.push({
            id: focusablePartId.SECONDS,
            text: settings.secondsSettings.text,
            maxLength: settings.secondsSettings.maxLength,
            milliseconds: 1000
        });
    }
    return result;
};

const defaultOptions = () => ({
    hoursPerDay: 24,
    allowDays: true,
    allowHours: true,
    allowMinutes: true,
    allowSeconds: true,
    showTitle: true,
    showEmptyParts: false,
    hideClearButton: false,
    fillZero: false,
    normalTime: false,
    class: '',
    emptyPlaceholder: Localizer.get('CORE.FORM.EDITORS.DURATION.NOTSET'),
    max: undefined,
    days: undefined,
    hours: undefined,
    minutes: undefined,
    seconds: undefined,
    min: undefined
    // allFocusableParts: undefined,
    // seconds: undefined // days, minutes, hours
});

const stateModes = {
    EDIT: 'edit',
    VIEW: 'view'
};

/**
 * @name DurationEditorView
 * @memberof module:core.form.editors
 * @class Inline duration editor. Supported data type: <code>String</code> in ISO8601 format (for example: 'P4DT1H4M').
 * @extends module:core.form.editors.base.BaseEditorView
 * @param {Object} options Options object. All the properties of {@link module:core.form.editors.base.BaseEditorView BaseEditorView} class are also supported.
 * @param {Number} [options.hoursPerDay=24] The amount of hours per day. The intended use case is counting work days taking work hours into account.
 * The logic is disabled by default: a day is considered as 24 hours.
 * @param {Boolean} [options.allowDays=true] Whether to display the day segment. At least one segment must be displayed.
 * @param {Boolean} [options.allowHours=true] Whether to display the hour segment. At least one segment must be displayed.
 * @param {Boolean} [options.allowMinutes=true] Whether to display the minute segment. At least one segment must be displayed.
 * @param {Boolean} [options.allowSeconds=true] Whether to display the second segment. At least one segment must be displayed.
 * @param {Boolean} {options.showTitle=true} Whether to show title attribute.
 * @param {Object}  [seconds] second Options
 * @param {Number}  [seconds.maxLength=4] Max digit capacity of seconds
 * @param {Number}  [seconds.text=(localization)] Separator. Show after part.
 * Similar options for days, hours, minutes. If all options are similar, use @param {Object} [allFocusableParts] by default.
 * @param {Object, String, Number} [max, min] Max, min value. Type - like arg for moment.duration
 * */

export default formRepository.editors.Duration = BaseEditorView.extend({
    initialize(options = {}) {
        this.__applyOptions(options, defaultOptions);
        this.focusableParts = createFocusableParts(this.options);

        this.state = {
            mode: stateModes.VIEW,
            displayValue: dateHelpers.durationISOToObject(this.value)
        };
    },

    template: Handlebars.compile(template),

    focusElement: '.js-input',

    className: 'js-duration editor editor_duration',

    ui: {
        input: '.js-input',
        clear: '.js-clear-button'
    },

    regions: {
        durationRegion: 'js-duration'
    },

    events: {
        'click @ui.clear': '__clear',
        'focus @ui.input': '__focus',
        'click @ui.input': '__onClick',
        'blur @ui.input': '__onBlur',
        'keydown @ui.input': '__keydown',
        'keyup @ui.input': '__keyup',
        mouseenter: '__onMouseenter'
    },

    __clear() {
        this.triggeredByClean = true;
        this.__updateState({
            mode: stateModes.VIEW,
            displayValue: null
        });
        this.__value(null, false);
        this.focus();
    },

    __onClick() {
        this.trigger('click');
        this.__focus();
    },

    __focus() {
        if (this.readonly) {
            return;
        }
        const curretPos = this.getCaretPos();
        this.__updateState({
            mode: stateModes.EDIT
        });
        const pos = this.fixCaretPos(curretPos);
        this.setCaretPos(pos);
    },

    __onBlur() {
        if (this.state.mode === stateModes.VIEW) {
            return;
        }

        this.__updateValueByInput(true);
    },

    __updateValueByInput(updateState) {
        let valueObject = this.__getObjectValueFromInput();

        valueObject = this.__checkMaxMinObject(valueObject, this.options.max, this.options.min);

        if (updateState) {            
            this.__updateState({
                mode: stateModes.VIEW,
                displayValue: valueObject
            });
        }

        if (this.triggeredByClean) {
            this.triggeredByClean = false;
            if (Object.values(valueObject).every(value => value === 0)) {
                this.ui.input.val(null);
                this.__value(null, true);
                return;
            }
        }
        this.__value(moment.duration(valueObject).toISOString(), true);
    },

    __getObjectValueFromInput() {
        const values = this.getSegmentValue();
        const valueObject = {
            days: 0,
            hours: 0,
            minutes: 0,
            seconds: 0
        };
        this.focusableParts.forEach((seg, i) => {
            valueObject[seg.id] = Number(values[i]);
        });

        const days = valueObject.days;

        const devider = 24 / this.options.hoursPerDay;

        const realDays = Math.floor(days / devider);
        const daysRemainder = days % devider;

        valueObject.days = realDays;
        valueObject.hours += daysRemainder * this.options.hoursPerDay;

        return valueObject;
    },

    getCaretPos() {
        return this.ui.input[0].selectionStart;
    },

    fixCaretPos(pos) {
        let resultPosition;
        const index = this.getSegmentIndex(pos);
        const focusablePart1 = this.focusableParts[index];
        const focusablePart2 = this.focusableParts[index + 1];
        if (pos >= focusablePart1.start && pos <= focusablePart1.end) {
            resultPosition = pos;
        } else if (pos > focusablePart1.end && (focusablePart2 ? pos < focusablePart2.start : true)) {
            resultPosition = focusablePart1.end;
        }
        return resultPosition !== undefined ? resultPosition : focusablePart1.start;
    },

    setCaretPos(pos) {
        this.ui.input[0].setSelectionRange(pos, pos);
    },

    moveCaret(delta) {
        this.setCaretPos(this.getCaretPos() + delta);
    },

    getSegmentIndex(pos) {
        // returns the index of the segment where we are at
        let i;
        let segmentIndex;

        segmentIndex = this.focusableParts.length - 1;
        this.initSegmentStartEnd();
        for (i = 0; i < this.focusableParts.length; i++) {
            const focusablePart1 = this.focusableParts[i];
            const focusablePart2 = this.focusableParts[i + 1];
            if (focusablePart1.start <= pos && pos <= focusablePart1.end) {
                // the position is within the first segment
                segmentIndex = i;
                break;
            }
            if (focusablePart2) {
                if (focusablePart1.end < pos && pos < focusablePart2.start) {
                    const whitespaceLength = 1;
                    if (pos < focusablePart2.start - whitespaceLength) {
                        // the position is at '1 <here>d 2 h' >> first fragment
                        segmentIndex = i;
                    } else {
                        // the position is at '1 d<here> 2 h' >> second fragment
                        segmentIndex = i + 1;
                    }
                    break;
                }
            }
        }
        return segmentIndex;
    },

    getSegmentValue(index) {
        const segments = [];
        for (let i = 0; i < this.focusableParts.length; i++) {
            // matches '123 d' segment
            segments.push('(\\S*)\\s+\\S*');
        }
        const regexStr = `^\\s*${segments.join('\\s+')}$`;
        let result = new RegExp(regexStr, 'g').exec(this.ui.input.val());
        if (!result) {
            result = this.focusableParts.map(() => '0');
            result.push('0');
        }
        return index !== undefined ? result[index + 1] : result.slice(1, this.focusableParts.length + 1);
    },

    getCurrentSegmentValue() {
        return this.getSegmentValue(this.getSegmentIndex(this.getCaretPos()));
    },

    setSegmentValue(index, value, replace) {
        let val = this.getSegmentValue(index);
        val = !replace ? parseInt(val) + value : value;
        if (val < 0) {
            return false;
        }
        if (this.options.normalTime) {
            val = this.__floorTime(val, this.focusableParts[index].id);
        }
        val = val.toString();
        if (val.length > this.focusableParts[index].maxLength) {
            return false;
        }
        const str = this.ui.input.val();
        this.ui.input.val(str.substr(0, this.focusableParts[index].start) + val + str.substr(this.focusableParts[index].end));
        return true;
    },

    atSegmentEnd(position) {
        const index = this.getSegmentIndex(position);
        return position === this.focusableParts[index].end;
    },

    atSegmentStart(position) {
        const index = this.getSegmentIndex(position);
        return position === this.focusableParts[index].start;
    },

    __value(value, triggerChange) {
        if (value === this.value) {
            return;
        }
        this.value = value;
        this.__updateEmpty();

        if (triggerChange) {
            this.__triggerChange();
        }
    },

    __keydown(event) {
        // this.ui.input[0].hasAttribute('readonly') for time editor, because setReadonly set tabindex -1
        if (event.ctrlKey || !this.getEditable() || this.ui.input[0].hasAttribute('readonly')) {
            return;
        }
        const position = this.getCaretPos();
        const index = this.getSegmentIndex(position);
        const focusablePart = this.focusableParts[index];
        switch (event.keyCode) {
            case keyCode.UP:
                if (this.setSegmentValue(index, 1)) {
                    this.initSegmentStartEnd();
                    this.setCaretPos(focusablePart.end);
                }
                return false;
            case keyCode.DOWN:
                if (this.setSegmentValue(index, -1)) {
                    this.initSegmentStartEnd();
                    this.setCaretPos(focusablePart.end);
                }
                return false;
            case keyCode.PAGE_UP:
                if (this.setSegmentValue(index, 10)) {
                    this.initSegmentStartEnd();
                    this.setCaretPos(focusablePart.end);
                }
                return false;
            case keyCode.PAGE_DOWN:
                if (this.setSegmentValue(index, -10)) {
                    this.initSegmentStartEnd();
                    this.setCaretPos(focusablePart.end);
                }
                return false;
            case keyCode.LEFT:
                if (this.atSegmentStart(position)) {
                    this.__setCaretToPreviousPart(index);
                    return false;
                }
                break;
            case keyCode.RIGHT:
                if (this.atSegmentEnd(position)) {
                    this.__setCaretToNextPart(index);
                    return false;
                }
                break;
            case keyCode.DELETE:
                if (this.atSegmentEnd(position)) {
                    this.__setCaretToNextPart(index);
                    return false;
                }
                if (this.getCurrentSegmentValue().length === 1) {
                    this.__replaceModeFor(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '0', 'right');
                    this.moveCaret(1);
                    return false;
                }
                break;
            case keyCode.BACKSPACE:
                if (this.atSegmentStart(position)) {
                    this.__setCaretToPreviousPart(index);
                    return false;
                }
                if (this.getCurrentSegmentValue().length === 1) {
                    this.__replaceModeFor(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '0', 'left');
                    this.moveCaret(-1);
                    return false;
                }
                break;
            case keyCode.ESCAPE:
                this.ui.input.blur();
                this.__updateState({
                    mode: stateModes.VIEW
                });
                break;
            case keyCode.ENTER:
                break;
            case keyCode.TAB: {
                const delta = event.shiftKey ? -1 : 1;
                if (this.focusableParts[index + delta]) {
                    this.setCaretPos(this.focusableParts[index + delta].start);
                    return false;
                }
                break;
            }
            case keyCode.HOME:
                this.setCaretPos(this.focusableParts[0].start);
                return false;
            case keyCode.END:
                this.setCaretPos(this.focusableParts[this.focusableParts.length - 1].end);
                return false;
            default: {
                let charValue = null;
                if (event.keyCode >= keyCode.NUM_0 && event.keyCode <= keyCode.NUM_9) {
                    charValue = event.keyCode - keyCode.NUM_0;
                } else if (event.keyCode >= keyCode.NUMPAD_0 && event.keyCode <= keyCode.NUMPAD_9) {
                    charValue = event.keyCode - keyCode.NUMPAD_0;
                }
                const valid = charValue !== null;
                if (!valid) {
                    return false;
                }
                if (this.getCurrentSegmentValue() === '0' && this.atSegmentEnd(position)) {
                    this.__replaceModeFor(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '', 'left');
                    this.moveCaret(-1);
                } else {
                    this.__replaceModeFor(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
                }
                if (this.getSegmentValue(index).length >= focusablePart.maxLength) {
                    return false;
                }
            }
        }

        this.trigger('keydown', event);
    },

    __keyup(event) {
        if (event.ctrlKey || this.readonly) {
            return;
        }
        if (
            (event.keyCode >= keyCode.NUM_0 && event.keyCode <= keyCode.NUM_9) ||
            (event.keyCode >= keyCode.NUMPAD_0 && event.keyCode <= keyCode.NUMPAD_9) ||
            event.keyCode === keyCode.DELETE
        ) {
            const position = this.getCaretPos();
            const index = this.getSegmentIndex(position);
            const focusablePart = this.focusableParts[index];
            const segmentValue = this.getSegmentValue(index);
            if (segmentValue.length < focusablePart.maxLength) {
                return;
            }
            if (this.atSegmentEnd(position)) {
                if (this.options.normalTime) {
                    this.setSegmentValue(index, this.__floorTime(segmentValue, focusablePart.id), true);
                }
                this.__setCaretToNextPart(index);
            }
        }
        if (event.keyCode === keyCode.BACKSPACE) {
            const position = this.getCaretPos();
            const index = this.getSegmentIndex(position);
            const focusablePart = this.focusableParts[index];
            if (this.getSegmentValue(index).length < focusablePart.maxLength) {
                return;
            }
            if (this.atSegmentStart(position)) {
                this.__setCaretToPreviousPart(index);
            }
        }
        if (event.keyCode === keyCode.ENTER) {
            this.__updateValueByInput();
        }
    },

    __floorTime(segValue, id) {
        const maxValue = {
            [focusablePartId.DAYS]: 31,
            [focusablePartId.HOURS]: 23,
            [focusablePartId.MINUTES]: 59,
            [focusablePartId.SECONDS]: 59
        };
        return Number(segValue) > maxValue[id] ? maxValue[id] : segValue;
    },

    __setCaretToNextPart(index) {
        if (this.focusableParts[index + 1]) {
            this.setCaretPos(this.focusableParts[index + 1].start);
        }
    },

    __setCaretToPreviousPart(index) {
        if (this.focusableParts[index - 1]) {
            this.setCaretPos(this.focusableParts[index - 1].end);
        }
    },

    __replaceModeFor(arrChar, insertChar = '', direction = 'right') {
        const inpValue = this.ui.input.val();
        const dirClarity = direction === 'left' ? 1 : 0;
        const caretPos = this.getCaretPos();
        const valueAfterCaret = inpValue[caretPos - dirClarity];
        if (arrChar.some(char => char === valueAfterCaret)) {
            this.ui.input.val(this.__replaceChar(inpValue, caretPos - dirClarity, insertChar));
            this.setCaretPos(caretPos);
        }
    },

    __replaceChar(str, i, insertChar = '') {
        return str.substr(0, i) + insertChar + str.slice(i + 1);
    },

    initSegmentStartEnd() {
        const values = this.getSegmentValue();
        let start = 0;
        for (let i = 0; i < this.focusableParts.length; i++) {
            const focusablePart = this.focusableParts[i];
            if (i > 0) {
                // counting whitespace before the value of the segment
                start++;
            }
            focusablePart.start = start;
            focusablePart.end = focusablePart.start + values[i].length;
            start = focusablePart.end + focusablePart.text.length;
        }
    },

    __createInputString(value, editable) {
        // The methods creates a string which reflects current mode (view/edit) and value.
        // The string is set into UI in __updateState

        const isNull = value === null;
        const seconds = !isNull ? value.seconds : 0;
        const minutes = !isNull ? value.minutes : 0;
        const hours = !isNull ? value.hours : 0;
        const days = !isNull ? value.days : 0;
        const data = {
            [focusablePartId.DAYS]: days,
            [focusablePartId.HOURS]: hours,
            [focusablePartId.MINUTES]: minutes,
            [focusablePartId.SECONDS]: seconds
        };

        if (!editable && isNull) {
            // null value is rendered as empty text
            return '';
        }
        if (!this.options.showEmptyParts && !editable) {
            const filledSegments = this.focusableParts.filter(part => Boolean(data[part.id]));
            if (filledSegments.length > 0) {
                // returns string like '0d 4h 32m'
                return filledSegments.reduce((p, seg) => `${p}${data[seg.id]}${seg.text} `, '').trim();
            }
            // returns string like '0d'
            return `0${this.focusableParts[0].text}`;
        }
        // always returns string with all editable segments like '0 d 5 h 2 m'
        return this.focusableParts
            .map(seg => {
                const val = data[seg.id];
                let valStr = this.options.showEmptyParts ? String(val) : _.isNumber(val) ? String(val) : '';
                valStr = this.options.fillZero ? this.__fillZero(valStr, seg.maxLength) : valStr;
                return valStr + seg.text;
            })
            .join(' ');
    },

    __fillZero(string, length) {
        const mask = '000000000000';
        return (mask + string).slice(-length);
    },

    __normalizeDuration(object) {
        // Data normalization:
        // Object like this: { days: 2, hours: 3, minutes: 133, seconds: 0 }
        // Is converted into this: { days: 2, hours: 5, minutes: 13, seconds: 0 }
        // But if hours segment is disallowed, it will look like this: { days: 2, hours: 0, minutes: 313, seconds: 0 } // 313 = 133 + 3*60

        if (object === null) {
            return null;
        }

        let totalMilliseconds = moment.duration(object).asMilliseconds();
        const result = {
            days: 0,
            hours: 0,
            minutes: 0,
            seconds: 0
        };
        this.focusableParts.forEach(seg => {
            result[seg.id] = Math.floor(totalMilliseconds / seg.milliseconds);
            totalMilliseconds %= seg.milliseconds;
        });
        return result;
    },

    setValue(value, triggerChange) {
        this.__value(value, triggerChange);
        this.__updateState({
            mode: stateModes.VIEW,
            displayValue: dateHelpers.durationISOToObject(value)
        });
    },

    __updateState(newState) {
        // updates inner state variables
        // updates UI

        if (!newState.mode || (newState.mode === stateModes.EDIT && newState.displayValue !== undefined)) {
            helpers.throwInvalidOperationError("The operation is inconsistent or isn't supported by this logic.");
        }

        if (this.state.mode === newState.mode && newState.mode === stateModes.EDIT) {
            return;
        }

        this.state.mode = newState.mode;
        if (newState.displayValue !== undefined) {
            this.state.displayValue = newState.displayValue;
        }

        const normalizedDisplayValue = this.__normalizeDuration(this.state.displayValue);
        const inEditMode = this.state.mode === stateModes.EDIT;
        const val = this.__createInputString(normalizedDisplayValue, inEditMode);
        this.ui.input.val(val);
        if (this.options.showTitle && !inEditMode) {
            this.$editorEl.prop('title', val);
        }
    },

    __onMouseenter() {
        this.$editorEl.off('mouseenter');

        if (!this.options.hideClearButton) {
            this.renderIcons(iconWrapNumber, iconWrapRemove);
        }
    },

    __checkMaxMinObject(valueObject, maxValue, minValue) {
        let value = moment.duration(valueObject).asMilliseconds();
        if (maxValue != null) {
            const max = moment.duration(maxValue).asMilliseconds();
            value = value > max ? max : value;
        }
        if (minValue != null) {
            const min = moment.duration(minValue).asMilliseconds();
            value = value < min ? min : value;
        }
        return dateHelpers.durationToObject(value);
    }
});
