/**
 * Created by rburson on 7/25/16.
 */

import * as React from 'react'
import {
    CvState,
    CvProps,
    CvEvent,
    CvEventType,
    CvEventRegistry,
    CvResultCallback,
    CvContext,
    CvNavigationResult,
    CvNavigationResultUtil,
    CvStateChangeResult,
    CvStateChangeType,
    CvMessage,
    CvMessageType,
    CvValueProvider,
    CvActionFiredResult,
    CvActionHandlerParams,
    CvActionBase,
    CvResourceManager
} from './catreact-core'
import {
    FormContext,
    EntityRec,
    EntityRecDef,
    EditorContext,
    Try,
    Either,
    NavRequest,
    PaneMode,
    Prop,
    PropDef,
    Log,
    PropFormatter,
    DialogException
} from 'catavolt-sdk'

export interface CvEditorBaseState extends CvState {
}

export interface CvEditorBaseProps extends CvProps {
    formContext?:FormContext;
    paneRef?:number;
    //called detailsContext for compatibility's sake
    detailsContext?:EditorContext;
    detailsRenderer?:(cvContext:CvContext, entityRec:EntityRec, callback:CvEditorPaneCallback)=>{}
    errorRenderer?:(cvContext:CvContext, dialogException?:DialogException)=>{};
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>;
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    actionListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    actionProvider?:CvValueProvider<CvActionHandlerParams>;
    initMode?:CvInitMode;
}

export interface CvEditorPaneCallback {
    clear():void;
    getPropValueForDisplay(propName:string):string;
    getPropValueForEdit(propName:string):string;
    openReadMode(resultCallback?:CvResultCallback<EntityRecDef>):void;
    openWriteMode(resultCallback?:CvResultCallback<EntityRecDef>):void;
    saveChanges(resultCallback?:CvResultCallback<NavRequest>, navTarget?:string):void;
    setPropValue(name:string, value:any):void;
    setBinaryPropWithDataUrl(name:string, dataUrl:string):void;
    setBinaryPropWithEncodedData(name:string, encodedData:string):void;
    suppressEvents(suppressEvents:boolean):void;
    refresh():void;
}

export enum CvInitMode {
    NORMAL, DEFERRED, MANUAL
}

/**
 * Render a EditorContext
 */
export var CvEditorBase = {

    editorContext: function (nextProps, nextContext) {

        let editorContext:EditorContext = null;
        let paneRef = null;

        if (nextProps) {
            if (nextProps.detailsContext) {
                editorContext = nextProps.detailsContext;
            } else if (nextProps.paneRef !== null && nextProps.paneRef !== undefined) {
                const formContext:FormContext = this.formContext(nextProps, nextContext);
                editorContext = this._getEditorContext(formContext, nextProps.paneRef);
            }
        } else {
            if (this.props.detailsContext) {
                editorContext = this.props.detailsContext;
            } else {
                const formContext:FormContext = this.formContext();
                editorContext = this._getEditorContext(formContext, this.props.paneRef);
            }
        }

        return editorContext;
    },

    formContext: function (nextProps, nextContext) {
        if (nextProps && nextProps.formContext) {
            return nextProps.formContext;
        } else if (nextContext) {
            return this.firstInScope(FormContext, nextContext.cvContext.scopeCtx);
        } else {
            return this.props.formContext || this.firstInScope(FormContext);
        }
    },

    getChildContext: function () {
        const ctx = this.getDefaultChildContext();
        ctx.cvContext.scopeCtx.scopeObj = this.editorContext();
        return ctx;
    },

    refresh: function (nextProps, nextContext, callback:CvResultCallback<any>) {

        const editorContext:EditorContext = this.editorContext(nextProps, nextContext);
        if (editorContext && this.isMounted()) {
            if (editorContext.isDestroyed) {
                callback && callback(true);
                if (this.isMounted()) {
                    this.forceUpdate();
                }
            } else {
                CvActionBase._publishActionStarted('#refresh', editorContext, false, this.props.actionListeners, this.eventRegistry());
                editorContext.read().onComplete((entityRecTry:Try<EntityRec>)=> {
                    if (entityRecTry.isFailure) {
                        const event:CvEvent<CvMessage> = {
                            type: CvEventType.MESSAGE,
                            eventObj: {
                                message: 'refresh(): failed to read editorContext',
                                messageObj: entityRecTry.failure,
                                type: CvMessageType.ERROR
                            }
                        }
                        this.eventRegistry().publish(event, false);
                        callback && callback(null, entityRecTry.failure);
                    } else {
                        callback && callback(true);
                        if (this.isMounted()) {
                            this.forceUpdate();
                        }
                    }
                    CvActionBase._publishActionFinished('#refresh', editorContext, this.props.actionListeners, this.eventRegistry());
                });
            }
        }
    },
    
    _checkDestroyed: function () {
        const editorContext:EditorContext = this.editorContext();
        if (editorContext && editorContext.isDestroyed) {
            this._publishStateChange(CvStateChangeType.DESTROYED);
        }
    },

    _componentDidMount: function () {
        if(!this.props.initMode || this.props.initMode === CvInitMode.NORMAL) this.refresh();
        if (this.props.actionProvider) {
            this.props.actionProvider.subscribe(this._handleAction);
        }
        (this.eventRegistry() as CvEventRegistry)
            .subscribe<CvStateChangeResult>(this._dataChangeListener, CvEventType.STATE_CHANGE);
    },

    _componentWillReceiveProps: function (nextProps, nextContext) {
        if (nextProps.actionProvider && nextProps.actionProvider !== this.props.actionProvider) {
            nextProps.actionProvider.subscribe(this._handleAction);
        }
        if(!this.props.initMode || this.props.initMode === CvInitMode.NORMAL){
            if(this.editorContext() !== this.editorContext(nextProps, nextContext)) {
                this.refresh(nextProps, nextContext);
            }
        }
    },

    _componentWillUnmount: function () {
        (this.eventRegistry() as CvEventRegistry).unsubscribe(this._dataChangeListener);
    },


    _dataChangeListener: function (dataChangeResult:CvEvent<CvStateChangeResult>) {
        if (dataChangeResult.eventObj.type === CvStateChangeType.DATA_CHANGE
            && dataChangeResult.eventObj.source !== this.editorContext()) {
            if (this.props.initMode && (this.props.initMode === CvInitMode.NORMAL
                || this.props.initMode === CvInitMode.DEFERRED)) {
                this.refresh();
            }
        }
    },
    
    _getDefaultProps: function () {
        return {
            formContext: null,
            paneRef: null,
            //called 'details' instead of 'editor' for compatibility's sake
            detailsContext: null,
            detailsRenderer: null,
            errorRenderer: null,
            navigationListeners: [],
            stateChangeListeners: [],
            actionListeners: [],
            actionProvider: null,
            initMode: CvInitMode.NORMAL
        }
    },

    _getEditorContext: function (formContext:FormContext, paneRef:number) {
        let editorContext = null;
        formContext.childrenContexts.some((childContext)=> {
            if (childContext instanceof EditorContext &&
                childContext.paneRef == paneRef) {
                editorContext = childContext;
                return true;
            } else {
                return false;
            }
        });
        return editorContext;
    },

    _getCallbackObj: function ():CvEditorPaneCallback {
        
        let suppressEvents:boolean = false;

        return {

            clear: ():void => {
                this.editorContext().entityRec.propNames.forEach((propName:string)=>{
                    this._getCallbackObj().setPropValue(propName, null)});
            },
            getPropValueForDisplay: (propName:string):string => {
                const editorContext = this.editorContext();
                const prop:Prop = editorContext.buffer.propAtName(propName);
                const propDef:PropDef = editorContext.entityRecDef.propDefAtName(propName); 
                return PropFormatter.formatForRead(prop, propDef); 
            },
            
            getPropValueForEdit: (propName:string):string => {
                const editorContext = this.editorContext();
                const prop:Prop = editorContext.buffer.propAtName(propName);
                const propDef:PropDef = editorContext.entityRecDef.propDefAtName(propName);
                return PropFormatter.formatForWrite(prop, propDef);
            },

            openReadMode: (resultCallback?:CvResultCallback<EntityRecDef>):void => {
                this.editorContext().changePaneMode(PaneMode.READ).onComplete((entityRecDefTry:Try<EntityRecDef>)=> {
                    if (entityRecDefTry.isSuccess) {
                        if(!suppressEvents) {
                            this._publishStateChange(CvStateChangeType.MODE_CHANGE_READ);
                            this._checkDestroyed();
                            this.refresh(null, null, resultCallback);
                        } else {
                            resultCallback(entityRecDefTry.success);
                        }
                    } else {
                        const event:CvEvent<CvMessage> = {
                            type: CvEventType.MESSAGE,
                            eventObj: {
                                message: 'Could not open read mode',
                                messageObj: entityRecDefTry.failure,
                                type: CvMessageType.ERROR
                            }
                        }
                        if(!suppressEvents) this.eventRegistry().publish(event, false);
                        resultCallback(null, entityRecDefTry.failure);
                    }
                });
            },

            openWriteMode: (resultCallback?:CvResultCallback<EntityRecDef>):void => {
                this.editorContext().changePaneMode(PaneMode.WRITE).onComplete((entityRecDefTry:Try<EntityRecDef>)=> {
                    if (entityRecDefTry.isSuccess) {
                        if(!suppressEvents) {
                            this.refresh(null, null, resultCallback);
                            this._publishStateChange(CvStateChangeType.MODE_CHANGE_WRITE);
                            this._checkDestroyed();
                        } else {
                            resultCallback(entityRecDefTry.success);
                        }
                    } else {
                        const event:CvEvent<CvMessage> = {
                            type: CvEventType.MESSAGE,
                            eventObj: {
                                message: 'Could not open edit mode',
                                messageObj: entityRecDefTry.failure,
                                type: CvMessageType.ERROR
                            }
                        }
                        if(!suppressEvents) this.eventRegistry().publish(event, false);
                        resultCallback(null, entityRecDefTry.failure);
                    }
                });
            },
            
            refresh: ()=> {
                this.refresh();
            },

            saveChanges: (resultCallback?:CvResultCallback<CvEvent<CvNavigationResult>>, navTarget?:string):void => {

                const editorContext = this.editorContext();

                editorContext.write().onComplete((writeTry:Try<Either<NavRequest, EntityRec>>)=> {
                    if (writeTry.isFailure) {
                        const event:CvEvent<CvMessage> = {
                            type: CvEventType.MESSAGE,
                            eventObj: {
                                message: 'Save changes failed',
                                messageObj: writeTry.failure,
                                type: CvMessageType.ERROR
                            }
                        }
                        if(!suppressEvents) this.eventRegistry().publish(event, false);
                        resultCallback(null, Error(writeTry.failure));
                    } else {
                        const either:Either<NavRequest, EntityRec> = writeTry.success;
                        const result:CvEvent<CvNavigationResult> = either.isLeft ? this._handleNavigation(either.left, '#write', navTarget, editorContext.isDestroyed) : null;
                        if(!suppressEvents) {
                            this._publishStateChange(CvStateChangeType.DATA_CHANGE);
                            this._checkDestroyed();
                            if (this.isMounted()) {
                                this.forceUpdate();
                            }
                        }
                        resultCallback(result);
                    }
                });
            },

            setBinaryPropWithDataUrl: (name:string, dataUrl:string) => {
                this.editorContext().setBinaryPropWithDataUrl(name, dataUrl);
            },

            setBinaryPropWithEncodedData: (name:string, encodedData:string) => {
                this.editorContext().setBinaryPropWithEncodedData(name, encodedData);
            },

            setPropValue: (name:string, value:any) => {
                Log.debug('setting: ' + name + ' : ' + value);
                const editorContext = this.editorContext();
                const parsedValue = editorContext.setPropValue(name, value);
                const propDef:PropDef = editorContext.propDefAtName(name);
                if (propDef && propDef.canCauseSideEffects) {
                    editorContext.processSideEffects(name, parsedValue).onComplete((propChangeTry:Try<void>)=> {
                        if (propChangeTry.isFailure) {
                            const event:CvEvent<CvMessage> = {
                                type: CvEventType.MESSAGE,
                                eventObj: {
                                    message: 'Process side effects failed',
                                    messageObj: propChangeTry.failure,
                                    type: CvMessageType.ERROR
                                }
                            }
                            if(!suppressEvents) this.eventRegistry().publish(event, false);
                        } else {
                            if (this.isMounted()) {
                                if(!suppressEvents) this.forceUpdate();
                            }
                        }
                    });
                }
            },
            
            suppressEvents: (val:boolean)=>{ suppressEvents = val; }

        }

    },

    _handleAction: function (params:CvActionHandlerParams) {

        const event:CvEvent<CvActionFiredResult> = params.event;
        const callback:CvResultCallback<any> = params.callback;
        if (event.eventObj.clientAction) {
            const actionId = event.eventObj.actionId;
            if (actionId === '#refresh') {
                this.refresh(null, null, callback);
            } else {
                callback(null, `Action ${actionId} not yet implemented.`);
            }
        }
    },

    _handleNavigation: function (navRequest:NavRequest, actionId:string, navTarget:string, sourceIsDestroyed:boolean):CvEvent<CvNavigationResult> {
        const resourceId = CvResourceManager.resourceIdForObject(navRequest, this.catavolt());
        const event:CvEvent<CvNavigationResult> =
        {
            type: CvEventType.NAVIGATION,
            resourceId: resourceId,
            eventObj: {
                navRequest: navRequest,
                actionId: actionId,
                navTarget: navTarget,
                sourceIsDestroyed: sourceIsDestroyed,
                type: CvNavigationResultUtil.determineType(navRequest)
            }
        };
        (this.eventRegistry() as CvEventRegistry).publish<CvNavigationResult>(event, CvResourceManager.shouldCacheResult(this.eventRegistry()));
        this.props.navigationListeners.forEach((listener)=> {
            listener(event)
        });
        return event;
    },

    _publishStateChange: function (type:CvStateChangeType) {
        const editorContext:EditorContext = this.editorContext();
        if (editorContext) {
            const event:CvEvent<CvStateChangeResult> =
            {
                type: CvEventType.STATE_CHANGE,
                resourceId: null,
                eventObj: {source: editorContext, type: type}
            };

            (this.eventRegistry() as CvEventRegistry).publish(event, false)

            this.props.stateChangeListeners.forEach(listener=> {
                listener(event)
            });
        }
    },

    _render: function () {

        const editorContext:EditorContext = this.editorContext();

        if (editorContext && !editorContext.isDestroyed) {
            
            if(editorContext.hasError) {
                return this.props.errorRenderer ? this.props.errorRenderer(this.getChildContext().cvContext, editorContext.error) : null;
            } else if (this.props.renderer) {
                return this.props.renderer(this.getChildContext().cvContext);
            } else if (this.props.detailsRenderer) {
                if (editorContext.buffer) {
                    return this.props.detailsRenderer(this.getChildContext().cvContext, editorContext.entityRec, this._getCallbackObj());
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } else {
            return null;
        }
    },


}