import {
    result,
    isFunction,
    isString,
    isArray,
    isUndefined,
    compact,
    map,
    each,
    extend,
    ListIterator,
} from 'underscore';
import { eachRight } from './underscore-compat';
import { View, Model } from 'backbone';

export type Result<T> = T | { (): T };
export type Selector = NonNullable<string>;
export type NoSelector = false;
export type FallbackSelector = Selector | NoSelector;
export type JQElement = JQuery<Element>;
export type SafeInsertionMethod = 'append' | 'prepend';
export type RiskyInsertionMethod = 'after' | 'before' | 'replaceWith';
export type InsertionMethod = SafeInsertionMethod | RiskyInsertionMethod;

export interface RootSubViewDton {
    view: string | Result<View>;
    selector?: Result<NoSelector>;
    method?: Result<SafeInsertionMethod>;
    place?: string | Result<boolean>;
}

export interface SelectorSubViewDton {
    view: string | Result<View>;
    selector: Result<Selector>;
    method?: Result<InsertionMethod>;
    place?: string | Result<boolean>;
}

export type SubViewDescription = RootSubViewDton | SelectorSubViewDton;
export type SubView = string | Result<View | SubViewDescription>;

export interface SubViewIterator {
    (view: View, selector: JQElement, method: InsertionMethod): void;
}

export interface IterationOptions {
    placeOnly?: boolean;
    reverse?: boolean;
}

const defaultIterationOptions: IterationOptions = {
    placeOnly: false,
    reverse: false,
};

export type _NormalizedSubView = [View, JQElement, InsertionMethod];

interface ContainerMap {
    [selector: string]: JQElement;
}

export default class CompositeView<TModel extends Model = Model> extends View<TModel> {
    defaultPlacement: SafeInsertionMethod;
    _containers: ContainerMap;

    render() {
        this.beforeRender();
        this.detachSubviews();
        this._resetContainer();
        this.placeSubviews();
        this.afterRender();
        return this;
    }

    beforeRender() { return this; }
    renderContainer() { return this; }
    afterRender() { return this; }

    remove(): this {
        this.clearSubviews();
        delete this._containers;
        super.remove();
        return this;
    }

    /**
     * Invariant: each subview must appear at most once in this list.
     */
    subviews(): SubView[] {
        return [];
    }

    clearSubviews(): this {
        return this.forEachSubview(_removeSubview, { reverse: true });
    }

    detachSubviews(): this {
        return this.forEachSubview(_detachSubview, { reverse: true });
    }

    placeSubviews(): this {
        // No need to detach the subview first.
        // Each subview is unique, so jQuery moves rather than copies it.
        return this.forEachSubview(_placeSubview, { placeOnly: true });
    }

    forEachSubview(iteratee: SubViewIterator, options?: IterationOptions):this {
        let _iteratee = args => iteratee.apply(this, args);
        let { placeOnly, reverse } = options || defaultIterationOptions;
        let iterator = reverse ? eachRight : each;
        let list = this._getSubviews(placeOnly);
        iterator(list, _iteratee);
        return this;
    }

    _resetContainer(): this {
        this._containers = {};
        return this.renderContainer();
    }

    _getSubviews(applyChecks: boolean): _NormalizedSubView[] {
        let normalize = this._normalizeSubview.bind(this, applyChecks);
        if (!this._containers) this._resetContainer();
        return compact(map(result(this, 'subviews'), normalize));
    }

    _normalizeSubview(applyChecks: boolean, sv: SubView): _NormalizedSubView | void {
        sv = this._resolve(sv);
        if (isUndefined(sv)) return null;
        if (sv instanceof View) return [sv, this.$el, this.defaultPlacement];
        let { view, selector, place } = sv;
        if (applyChecks && !isUndefined(place) && !this._resolve(place)) {
            return null;
        }
        view = this._resolve(view);
        if (!(view instanceof View)) return null;
        if (isFunction(selector)) {
            selector = selector.call(this);
        }
        if (isString(selector)) {
            let { method } = sv;
            return this._normalizeSelectorSubview(view, selector, method);
        } else {
            let { method } = sv as RootSubViewDton;
            return this._normalizeRootSubview(view, method);
        }
    }

    _normalizeRootSubview(
        view: View,
        method: Result<SafeInsertionMethod>,
    ): _NormalizedSubView {
        if (isFunction(method)) {
            method = method.call(this) as SafeInsertionMethod;
        }
        if (method && method !== 'append' && method !== 'prepend') {
            throw new TypeError(`Selector empty or attempting jQuery method ` +
                `.${method}() on the root element of a CompositeView.`);
        }
        return [view, this.$el, method || this.defaultPlacement];
    }

    _normalizeSelectorSubview(
        view: View,
        selector: Selector,
        method: Result<InsertionMethod>,
    ): _NormalizedSubView {
        if (isFunction(method)) method = method.call(this) as InsertionMethod;
        let container = this._containers[selector];
        if (!container) {
            this._containers[selector] = container = this.$(selector).first();
        }
        return [view, container, method || this.defaultPlacement];
    }

    _resolve<T>(handle: string | Result<T>): T {
        if (isString(handle)) handle = result(this, handle) as Result<T>;
        if (isFunction(handle)) handle = handle.call(this) as T;
        return handle;
    }
}

extend(CompositeView.prototype, {
    defaultPlacement: 'append',
});

export function _removeSubview<V extends View>(view: V): void {
    view.remove();
}

export function _detachSubview<V extends View>(view: V): void {
    view.$el.detach();
}

export function _placeSubview<V extends View>(
    subview: V,
    container: JQElement,
    method: InsertionMethod,
): void {
    const insert = (container[method]);
    insert.call(container, subview.el);
}
