import Face from './Face';
import List from './List';
import Group from './Group';
import Label from './Label';
import Timer from './Timer';
import Divider from './Divider';
import * as Faces from '../Faces';
import FaceValue from './FaceValue';
import DomComponent from './DomComponent';
import validate from '../Helpers/Validate';
import DefaultValues from '../Config/DefaultValues';
import ConsoleMessages from '../Config/ConsoleMessages';
import { flatten, isNull, isString, isObject, isUndefined, isFunction, error } from '../Helpers/Functions';

export default class FlipClock extends DomComponent {
   
    /**
     * Create a new `FlipClock` instance.
     *
     * @class FlipClock
     * @extends DomComponent
     * @param {HTMLElement} el - The HTML element used to bind clock DOM node.
     * @param {*} value - The value that is passed to the clock face.
     * @param {object|undefined} attributes - {@link FlipClock.Options} passed an object with key/value.
     */
        
    /**
     * @namespace FlipClock.Options
     * @classdesc An object of key/value pairs that will be used to set the attributes.
     * 
     * ##### Example:
     * 
     *     {
     *        face: 'DayCounter',
     *        language: 'es',
     *        timer: Timer.make(500)
     *     }
     * 
     * @property {string|Face} [face={@link Faces.DayCounter}] - The clock's {@link Face} instance.
     * @property {number} [interval=1000] - The clock's interval rate (in milliseconds).
     * @property {object} [theme={@link Themes.Original}] - The clock's theme.
     * @property {string|object} [language={@link Languages.English}] - The clock's language.
     * @property {Timer} [timer={@link Timer}] - The clock's timer.
     */
    
    constructor(el, value, attributes) {
        if(!validate(el, HTMLElement)) {
            error(ConsoleMessages.element);
        }

        if(isObject(value) && !attributes) {
            attributes = value;
            value = undefined;
        }

        const face = attributes.face || DefaultValues.face;

        delete attributes.face;

        super(Object.assign({
            originalValue: value,
            theme: DefaultValues.theme,
            language: DefaultValues.language,
            timer: Timer.make(attributes.interval || 1000),
        }, attributes));

        if(!this.face) {
            this.face = face;
        }

        this.mount(el);
    }

    /**
     * The clock `Face`.
     *
     * @type {Face}
     */
    get face() {
        return this.$face;
    }

    set face(value) {
        if(!validate(value, [Face, 'string', 'function'])) {
            error(ConsoleMessages.face);
        }

        this.$face = (Faces[value] || value).make(Object.assign(this.getPublicAttributes(), {
            originalValue: this.face ? this.face.originalValue : undefined
        }));

        this.$face.initialized(this);

        if(this.value) {
            this.$face.value = this.face.createFaceValue(this, this.value.value);
        }
        else if(!this.value) {
            this.value = this.originalValue;
        }

        this.el && this.render();
    }

    /**
     * The `stopAt` attribute.
     *
     * @type {*}
     */
    get stopAt() {
        return isFunction(this.$stopAt) ? this.$stopAt(this) : this.$stopAt;
    }

    set stopAt(value) {
        this.$stopAt = value;
    }

    /**
     * The `timer` instance.
     *
     * @type {Timer}
     */
    get timer() {
        return this.$timer;
    }

    set timer(timer) {
        if(!validate(timer, Timer)) {
            error(ConsoleMessages.timer);
        }

        this.$timer = timer;
    }

    /**
     * Helper method to The clock's `FaceValue` instance.
     *
     * @type {FaceValue|null}
     */
    get value() {
        return this.face ? this.face.value : null;
    }

    set value(value) {
        if(!this.face) {
            throw new Error('A face must be set before setting a value.');
        }

        if(value instanceof FaceValue) {
            this.face.value = value;
        }
        else if(this.value) {
            this.face.value = this.face.value.clone(value);
        }
        else {
            this.face.value = this.face.createFaceValue(this, value);
        }

        this.el && this.render();
    }

    /**
     * The `originalValue` attribute.
     *
     * @type {*}
     */
    get originalValue() {
        if(isFunction(this.$originalValue) && !this.$originalValue.name) {
            return this.$originalValue();
        }

        if(!isUndefined(this.$originalValue) && !isNull(this.$originalValue)) {
            return this.$originalValue;
        }

        return this.face ? this.face.defaultValue() : undefined;
    }

    set originalValue(value) {
        this.$originalValue = value;
    }

    /**
     * Mount the clock to the parent DOM element.
     *
     * @param  {HTMLElement} el - The parent `HTMLElement`.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    mount(el) {
        super.mount(el);

        this.face.mounted(this);

        return this;
    }

    /**
     * Render the clock's DOM nodes.
     *
     * @return {HTMLElement} - The parent `HTMLElement`.
     */
    render() {
        // Call the parent render function
        super.render();

        // Check to see if the face has a render function defined in the theme.
        // This allows a face to completely re-render or add to the theme.
        // This allows face specific interfaces for a theme.
        if(this.theme.faces[this.face.name]) {
            this.theme.faces[this.face.name](this.el, this);
        }

        // Pass the clock instance to the rendered() function on the face.
        // This allows global modifications to the rendered templates not
        // theme specific.
        this.face.rendered(this);

        // Return the rendered `HTMLElement`.
        return this.el;
    }

    /**
     * Start the clock.
     *
     * @param  {Function} fn - The interval callback.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    start(fn) {
        if(!this.timer.started) {
            this.value = this.originalValue;
        }

        isUndefined(this.face.stopAt) && (this.face.stopAt = this.stopAt);
        isUndefined(this.face.originalValue) && (this.face.originalValue = this.originalValue);

        this.timer.start(() => {
            this.face.interval(this, fn);
        });

        this.face.started(this);

        return this.emit('start');
    }

    /**
     * Stop the clock.
     *
     * @param  {Function} fn - The stop callback.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    stop(fn) {
        this.timer.stop(fn);
        this.face.stopped(this);

        return this.emit('stop');
    }

    /**
     * Reset the clock to the original value.
     *
     * @param  {Function} fn - The interval callback.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    reset(fn) {
        this.value = this.originalValue;
        this.timer.reset(() => this.interval(this, fn));
        this.face.reset(this);

        return this.emit('reset');
    }

    /**
     * Helper method to increment the clock's value.
     *
     * @param  {*|undefined} value - Increment the clock by the specified value.
     *     If no value is passed, then the default increment is determined by
     *     the Face, which is usually `1`.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    increment(value) {
        this.face.increment(this, value);

        return this;
    }

    /**
     * Helper method to decrement the clock's value.
     *
     * @param  {*|undefined} value - Decrement the clock by the specified value.
     *     If no value is passed, then the default decrement is determined by
     *     the `Face`, which is usually `1`.
     * @return {FlipClock} - The `FlipClock` instance.
     */
    decrement(value) {
        this.face.decrement(this, value);

        return this;
    }

    /**
     * Helper method to instantiate a new `Divider`.
     *
     * @param  {object|undefined} [attributes] - The attributes passed to the
     *     `Divider` instance.
     * @return {Divider} - The instantiated Divider.
     */
    createDivider(attributes) {
        return Divider.make(Object.assign({
            theme: this.theme,
            language: this.language
        }, attributes));
    }

    /**
     * Helper method to instantiate a new `List`.
     *
     * @param  {*} value - The `List` value.
     * @param  {object|undefined} [attributes] - The attributes passed to the
     *     `List` instance.
     * @return {List} - The instantiated `List`.
     */
    createList(value, attributes) {
        return List.make(value, Object.assign({
            theme: this.theme,
            language: this.language
        }, attributes));
    }

    /**
     * Helper method to instantiate a new `Label`.
     *
     * @param  {*} value - The `Label` value.
     * @param  {object|undefined} [attributes] - The attributes passed to the
     *     `Label` instance.
     * @return {Label} - The instantiated `Label`.
     */
    createLabel(value, attributes) {
        return Label.make(value, Object.assign({
            theme: this.theme,
            language: this.language
        }, attributes));
    }

    /**
     * Helper method to instantiate a new `Group`.
     *
     * @param  {array} items - An array of `List` items to group.
     * @param  {Group|undefined} [attributes] - The attributes passed to the
     *     `Group` instance.
     * @return {Group} - The instantiated `Group`.
     */
    createGroup(items, attributes) {
        return Group.make(items, Object.assign({
            theme: this.theme,
            language: this.language
        }, attributes));
    }

    /**
     * The `defaults` attribute.
     *
     * @type {object}
     */
    static get defaults() {
        return DefaultValues;
    }

    /**
     * Define the name of the class.
     *
     * @return {string}
     */
    static defineName() {
        return 'FlipClock';
    }

    /**
     * Helper method to set the default `Face` value.
     *
     * @param  {Face} value - The default `Face` class.This should be a
     *     constructor.
     * @return {void}
     */
    static setDefaultFace(value) {
        if(!validate(value, Face)) {
            error(ConsoleMessages.face);
        }

        DefaultValues.face = value;
    }

    /**
     * Helper method to set the default theme.
     *
     * @param {object} value - The default theme.
     * @return {void}
     */
    static setDefaultTheme(value) {
        if(!validate(value, 'object')) {
            error(ConsoleMessages.theme);
        }

        DefaultValues.theme = value;
    }

    /**
     * Helper method to set the default language.
     *
     * @param {object} value - The default language.
     * @return {void}
     */
    static setDefaultLanguage(value) {
        if(!validate(value, 'object')) {
            error(ConsoleMessages.language);
        }

        DefaultValues.language = value;
    }

}