import {RoundaboutReady} from './ts-refs/trans-render/froop/types';
import {assignGingerly} from 'trans-render/lib/assignGingerly.js';
import { RoundAbout } from 'trans-render/froop/roundabout.js';
import { EnhancementInfo, IEnhancement, BEAllProps} from './ts-refs/trans-render/be/types';
import { BEConfig, PropInfo, PropLookup} from './index';
import {dispatchEvent} from 'trans-render/positractions/dispatchEvent.js';
import { IMountObserver } from './ts-refs/mount-observer/types';
import {RRMixin} from 'trans-render/froop/RRMixin.js';
export {BEConfig} from './index';
const publicPrivateStore = Symbol();

export class BE<TProps = any, TActions=TProps, TElement extends Element = Element> 
    extends RRMixin(EventTarget) implements RoundaboutReady, IEnhancement<TElement>{



    propagator = new EventTarget();
    [publicPrivateStore]: Partial<TProps> = {};

    async covertAssignment(obj: TProps): Promise<void> {
        await assignGingerly(this[publicPrivateStore], obj) ;
    }

    #disconnectedAbortController = new AbortController();

    get disconnectedSignal(){
        return this.#disconnectedAbortController.signal;
    }

    get #config(){
        return (<any>this.constructor).config as BEConfig;
    }

    de = dispatchEvent;

    channelEvent(event: Event){
        (<any>event).enh = this.#ei?.mountCnfg?.enhPropKey;
        this.#enhancedElement!.dispatchEvent(event);
    }

    #enhancedElement: TElement | undefined;
    #ei: EnhancementInfo | undefined;
    get enhancedElement(){
        return this.#enhancedElement!;
    }
    async attach(el: TElement, enhancementInfo: EnhancementInfo){
        this.#enhancedElement = el;
        this.#ei = enhancementInfo;
        await this.covertAssignment({enhancedElement: el} as TProps);
        const props = (<any>this.constructor).props as PropLookup;
        await this.#propUp(props, enhancementInfo);
        await this.#instantiateRoundaboutIfApplicable(el);
    }


    /**
     * Needed for asynchronous loading
     * @param props Array of property names to "upgrade", without losing value set while element was Unknown
     * @param defaultValues:   If property value not set, set it from the defaultValues lookup
     * @private
     */
    async #propUp<T>(props: PropLookup, enhancementInfo: EnhancementInfo){
        const {initialPropValues} = enhancementInfo;
        const objToMerge: any = {...initialPropValues};
        for(const key in props){
            if(key in objToMerge) continue;
            const propInfo = props[key];
            const value = propInfo.def;
            if(value !== undefined){
                objToMerge[key] = value;
            }
            
        }
        await this.covertAssignment(objToMerge);
    }

    async detach(el: TElement){
        this.propagator.dispatchEvent(new Event('disconnectedCallback'));
        this.#disconnectedAbortController.abort();
    }

    #roundabout: RoundAbout | undefined;

    async #instantiateRoundaboutIfApplicable(container: TElement){
        
        const config = this.#config;
        const {actions, compacts, infractions, handlers, positractions, hitch, isSleepless} = config;
        if((actions || compacts || infractions || handlers || positractions) !== undefined){
            let mountObservers: Set<IMountObserver> | undefined;
            if(!isSleepless){
                const {guid} = await import('mount-observer/MountObserver.js');
                mountObservers = (<any>this)[guid];
            }
            const {roundabout} = await import('trans-render/froop/roundabout.js');
            const [vm, ra] = await roundabout({
                vm: this,
                container,
                actions,
                compacts,
                handlers,
                positractions,
                hitch,
                mountObservers
            }, infractions);
            this.#roundabout = ra;
        }
        
    }

    whenResolved(): Promise<boolean>{
        return new Promise((resolve, reject) => {
            if(this.rejected) {
                resolve(false);
                return;
                //reject(false);
            }
            if(this.resolved){
                resolve(true);
                return;
            }

            this.addEventListener('resolved', e => {
                if(this.resolved){
                    resolve(true);
                }
            });
        })
    }


    static config: BEConfig | undefined;

    static async bootUp(){
        const config = this.config!;
        const {propDefaults, propInfo} = config;
        const props = {...this.props as PropLookup};
        Object.assign(props, propInfo);
        if(propDefaults !== undefined){
            for(const key in propDefaults){
                const def = propDefaults[key];
                const propInfo = {
                    ...defaultProp,
                    def,
                    propName: key
                } as PropInfo;
                props[key] = propInfo;

            }
            
        }
        if(propInfo !== undefined){
            for(const key in propInfo){
                const prop = propInfo[key]!;
                const mergedPropInfo = {
                    ...defaultProp,
                    ...prop,
                    propName: key
                } as PropInfo
                props[key] = mergedPropInfo;
            }
        }
        this.props = props;
        this.addProps(this, props);
        
    }

    static addProps(newClass: {new(): BE}, props: PropLookup){
        const proto = newClass.prototype;
        for(const key in props){
            if(key in proto) continue;
            const prop = props[key];
            const {ro} = prop;
            if(ro){
                Object.defineProperty(proto, key, {
                    get(){
                        return this[publicPrivateStore][key];
                    },
                    enumerable: true,
                    configurable: true,
                });
            }else{
                Object.defineProperty(proto, key, {
                    get(){
                        return this[publicPrivateStore][key];
                    },
                    set(nv: any){
                        const ov = this[publicPrivateStore][key];
                        if(prop.dry && ov === nv) return;
                        this[publicPrivateStore][key] = nv;
                        (this as BE).propagator.dispatchEvent(new Event(key));
                    },
                    enumerable: true,
                    configurable: true,
                });
            }

        }
    }

    static props: PropLookup = {};
}

export interface BE<TProps = any, TActions=TProps, TElement extends Element = Element> extends BEAllProps, IEnhancement<TElement>{}

const defaultProp: PropInfo = {
    dry: true,
};

export const propDefaults: Partial<{[key in keyof BEAllProps]: IEnhancement[key]}> = {
    resolved: false,
    rejected: false,
}