/**
 * Created by rburson on 2/10/16.
 */

import * as React from 'react'
import {
    CvState,
    CvProps,
    CvBaseMixin,
    CvContext,
    CvEvent,
    CvEventRegistry,
    CvEventType,
    CvNavigationResult,
    CvActionFiredResult,
    CvScopeContext,
    CvValueProvider,
    CvMessage,
    CvMessageType,
    CvNavigationResultUtil,
    CvActionFiredResultType,
    CvResultCallback,
    CvStateChangeResult,
    CvStateChangeType,
    CvValueListener
} from './catreact-core'
import {
    PaneContext,
    QueryContext,
    EditorContext,
    FormContext,
    MenuDef,
    EntityRec,
    NavRequest,
    Future,
    Try,
    EntityRecUtil,
    ObjUtil,
    Log,
    DialogException, Either
} from 'catavolt-sdk'


/*
 Base class for all 'action' related components
 */

export interface CvActionBaseProps extends CvProps {
    /**
     * The actionId of this action
     */
    actionId?:string;
    /**
     * Array of {@link CvListener}s that will be notified of any actions fired
     */
    actionListeners?:Array<(event:CvEvent<CvActionFiredResult>)=>void>
    /**
     * If this callback function is provided, automatically fire this action immediately upon mounting and
     * return the result via the callback
     */
    fireOnLoad?:CvResultCallback<CvNavigationResult>;
    /**
     * The sdk {MenuDef} that points to this action. If not specified the containing sdk {PaneContext} will
     * be searched for the matching MenuDef
     */
    menuDef?:MenuDef;
    /**
     * Register to receive {@link CvEvent}s of type {@link CvNavigationResult}
     */
    navigationListeners?:Array<(event:CvEvent<CvNavigationResult>)=>void>
    /**
     * Provide a target for any navigations that originate from this action.  The navTarget should
     * correspond to the 'targetId' value of the coresident {@link CvNavigation} or {@link CvNavigator}.
     * This is useful for single page apps where components may be coresident.
     */
    navTarget?:string;
    /**
     * The sdk {PaneContext} associated with this action.  If not specified, the context chain will be upwardly
     * searched to find a component with a containing sdk {PaneContext} scopeObj
     */
    paneContext?:PaneContext;
    /**
     * Provides any target values that may have been 'selected' when firing this action
     */
    selectionProvider?:CvValueProvider<Array<string>>;
    /**
     * Render override function.  It should accept the following params:
     * @param cvContext The current cvContext.  The cvContext.cvScope.scopeObj will be the sdk {MenuDef} object
     * @param callback A callback for firing the action
     */
    renderer?:(cvContext:CvContext, callback?:CvActionCallback)=>{}
    /**
     * Name of the wrapping element (should be component name)
     * Defaults to 'span' from backward compatibility's sake
     */
    wrapperElemName?:string;
    /**
     * Props for the wrapping element
     */
    wrapperElemProps?:any;
    /**
     * Name of the wrapperElem property that fires the action (i.e. onClick or onPress)
     * Defaults to 'onClick'
     */
    wrapperEventHandlerName?:string;
    /**
     * Register to receive {@link CvEvent}s of type {@link CvStateChangeResult}
     */
    stateChangeListeners?:Array<(event:CvEvent<CvStateChangeResult>)=>void>;
    /**
     * Allows the specification of a handler for 'client' side actions.  This is a special class of actions
     * that require some type of special processing by the client. (i.e. #search, #refresh, #calulateStatistics, etc.)
     */
    actionHandler?:CvValueListener<CvActionHandlerParams>; 
}

export interface CvActionBaseState extends CvState {
    selectedTargets:Array<string>;
}

export interface CvActionHandlerParams {
    event:CvEvent<CvActionFiredResult>;
    callback:CvResultCallback<any>;
}

export interface CvActionCallback {
    fireAction(resultCallback?:CvResultCallback<CvNavigationResult>):void;
}

export var CvActionBase = {

    /*
     Find a PaneContext in the hierarchy that has a MenuDef with this actionId
     */
    findPaneContextWithActionId: function (actionId:string) {
        let paneContext:PaneContext = null;
        if (this.props.paneContext) {
            const menuDef = this.props.paneContext.findMenuDefAt(actionId);
            if (menuDef) {
                paneContext = this.props.paneContext;
            }
        } else {
            let scopeCtx:CvScopeContext = this.findFirstScopeCtx((scopeCtx:CvScopeContext)=> {
                if (scopeCtx.scopeObj instanceof PaneContext) {
                    return (scopeCtx.scopeObj as PaneContext).findMenuDefAt(actionId) != null;
                } else {
                    return false;
                }
            });
            if (scopeCtx) {
                paneContext = scopeCtx.scopeObj;
            }
        }
        return paneContext;
    },

    getSelectedTargets: function () {
        return this.state.selectedTargets || [];
    },

    paneContext: function () {
        var paneContext:PaneContext = null;
        if (this.props.paneContext) {
            paneContext = this.props.paneContext;
        } else if (this.props.actionId) {
            paneContext = this.findPaneContextWithActionId(this.props.actionId);
        }
        return paneContext ? paneContext : this.findPaneContext();
    },

    menuDef: function () {
        if (this.props.menuDef) {
            return this.props.menuDef;
        } else {
            const paneContext:PaneContext = this.paneContext();
            return paneContext.findMenuDefAt(this.props.actionId);
        }
    },

    performAction: function (resultCallback?:CvResultCallback<CvNavigationResult>, presaveExecuted?:boolean, destroyRequested?:boolean) {

        const paneContext = this.paneContext();
        if (paneContext) {
            const menuDef:MenuDef = this.menuDef();
            if (menuDef) {
                let result:Future<NavRequest> = null;
                /*
                 * Only navigate if the action is a not a 'client-side' action.
                 * If it is, the 'action' will still be published so that the client can handle it appropriately.
                 */
                const actionMeta = this._getActionMeta(menuDef.actionId);
                if (!actionMeta.isClientAction) {
                    if (paneContext instanceof QueryContext) {
                        result = (paneContext as QueryContext).performMenuAction(menuDef, this.getSelectedTargets());
                    } else if (paneContext instanceof EditorContext) {
                        const editorContext:EditorContext = paneContext as EditorContext;
                        const entityRec:EntityRec = editorContext.entityRec;
                        /* @TODO maybe we want to save changes here, maybe not... they are currently discarded */
                        const pendingWrites = EntityRecUtil.newEntityRec(entityRec.objectId, []);
                        if (editorContext.isWriteMode && menuDef.isPresaveDirective && !presaveExecuted) {
                            // Perform the write if needed.  By design, if there is a navigation as a result
                            // of the write it is ignored.  The purpose of the write is to setup the action.
                            let writeResult:Future<Either<NavRequest,EntityRec>> = editorContext.write({"followupAction":menuDef.actionId});
                            writeResult.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
                                        }
                                    }
                                    this.eventRegistry().publish(event, false);
                                    if(resultCallback && typeof resultCallback === 'function') {
                                        resultCallback(null, Error(writeTry.failure));
                                    }
                                } else {
                                    // With a successful write, proceed with executing the action.
                                    this.performAction(resultCallback, true, editorContext.isDestroyRequested);
                                }
                            });
                        } else {
                            result = editorContext.performMenuAction(menuDef, pendingWrites);
                        }
                    } else if (paneContext instanceof FormContext) {
                        result = (paneContext as FormContext).performMenuAction(menuDef);
                    }
                }

                /*
                 * Client Action - no server involvement
                 * Note: client actions are handled supplying an 'actionHandler' propery to the CvAction
                 * resultCallbacks will only be called for successful or failed 'Navigations' (non-client actions)
                 */
                if(actionMeta.isClientAction) {
                    /* let the designated handler perform the action */
                    if(this.props.actionHandler) {
                        /* Publish the action fired */
                        const event = this._publishActionStarted(menuDef.actionId, paneContext, actionMeta.isClientAction, this.props.actionListeners, this.eventRegistry());
                        this.props.actionHandler({event: event, callback: (success, error)=>{
                            if(error) {
                                const de = new DialogException();
                                de.message = '' + error;
                                const event:CvEvent<CvMessage> = {
                                    type: CvEventType.MESSAGE, eventObj: { message: '' + error, messageObj: de, type: CvMessageType.ERROR}
                                }
                                this.eventRegistry().publish(event, false);
                            }
                            /* Publish the action finished */
                            this._publishActionFinished(menuDef.actionId, paneContext, this.props.actionListeners, this.eventRegistry());
                        }});
                    } else {
                        /* If there's no handler for a client action, publish both start and finish events */
                        this._publishActionStarted(menuDef.actionId, paneContext, actionMeta.isClientAction, this.props.actionListeners, this.eventRegistry());
                        this._publishActionFinished(menuDef.actionId, paneContext, this.props.actionListeners, this.eventRegistry());
                    }

                /*
                 *  Otherwise the server has supplied us with a result
                */
                } else if (result) {
                    if (destroyRequested) {
                        paneContext.destroy();
                    }
                    /* Publish the action fired */
                    const event = this._publishActionStarted(menuDef.actionId, paneContext, actionMeta.isClientAction, this.props.actionListeners, this.eventRegistry());
                    /* Publish the navigation */
                    result.onComplete((navRequestTry:Try<NavRequest>) => {
                        /* Publish the action finished */
                        this._publishActionFinished(menuDef.actionId, paneContext, this.props.actionListeners, this.eventRegistry());

                        if (navRequestTry.isSuccess) {
                           const e:CvEvent<CvNavigationResult> =
                               CvNavigationResultUtil.publishNavigation(this.catavolt(), this.eventRegistry(), navRequestTry.success,
                               this.props.actionId, null, this.props.navTarget, this.props.navigationListeners,
                               paneContext.isDestroyed, actionMeta.noTransition);

                            if(resultCallback && typeof resultCallback === 'function'){
                                resultCallback(e.eventObj);
                            }

                            this._checkDestroyed(paneContext);
                        } else {
                            const event:CvEvent<CvMessage> = {type:CvEventType.MESSAGE, eventObj:{message:'Navigation failed',
                                messageObj:navRequestTry.failure, type: CvMessageType.ERROR}}
                            if(resultCallback && typeof resultCallback === 'function') {
                                resultCallback(null, navRequestTry.failure);
                            }
                            this.eventRegistry().publish(event, false);
                            if (destroyRequested) {  // If a presave, it may be destroyed even if the action failed
                                this._checkDestroyed(paneContext);
                            }
                        }

                    });
                }
            }
        } else {
            Log.info("Failed to find menuDef for action: " + this.props.actionId);
        }
    },

    _checkDestroyed: function (paneContext:PaneContext) {
        if (paneContext && paneContext.isDestroyed) {
            const event:CvEvent<CvStateChangeResult> =
            {
                type: CvEventType.STATE_CHANGE,
                resourceId: null,
                eventObj: {source: paneContext, type: CvStateChangeType.DESTROYED}
            };
            this.props.stateChangeListeners.forEach(listener=> {
                listener(event)
            });
        }
    },
    
    _componentDidMount: function() {

        if(this.props.selectionProvider) {
           this.props.selectionProvider.subscribe(this._setSelection);
        }
        if(this.props.fireOnLoad) {
            this._getCallbackObj().fireAction(this.props.fireOnLoad);
        }
    },

    _componentWillReceiveProps: function(nextProps) {
        if(nextProps.selectionProvider && (nextProps.selectionProvider != this.props.selectionProvider)) {
            nextProps.selectionProvider.subscribe(this._setSelection);
        }
        if(nextProps.fireOnUpdate) {
            this._getCallbackObj().fireAction(nextProps.fireOnUpdate);
        }
    },

    _shouldComponentUpdate: function(nextProps, nextState) {

        //don't update for a selection change
        if(this.state.selectedTargets != null && nextState.selectedTargets != null)  {
            if(this.state.selectedTargets.length != nextState.selectedTargets.length) {
               return false;
            }
        }
    },

    _getCallbackObj: function ():CvActionCallback {

        return {
            fireAction: (resultCallback?:CvResultCallback<CvNavigationResult>):void => {
                this.performAction(resultCallback);
            }
        }
    },

    _getInitialState: function() {
        return {selectedTargets: []}
    },

    _setSelection(selectedTargets:Array<string>) {
        this.setState({selectedTargets: selectedTargets});
    },


    /** static methods */

    /* static */
    /* intended for resuse by other components */
    _getActionMeta: function(actionId:string):{isClientAction; noTransition} {

        //treat search like a server action
        //searchQuery currently has a server-side bug if null is submitted for 'keyword'
        //if(actionId === "#search" || actionId === 'searchQuery') {
        if(actionId === "#search") {
            return {isClientAction: false, noTransition: true}
        } else {
            const isClientAction = actionId === 'clear' || (actionId.substr(0, 1) === "#");
            return {isClientAction: isClientAction, noTransition: false}
        }

    },

    /* static */
    /* intended for resuse by other components */
    _publishActionStarted: function (actionId:string,
                                     source:PaneContext,
                                     clientAction:boolean,
                                     actionListeners:Array<(event:CvEvent<CvActionFiredResult>)=>void>,
                                     eventRegistry:CvEventRegistry):CvEvent<CvActionFiredResult> {
        
        const e:CvEvent<CvActionFiredResult> = {
            type: CvEventType.ACTION_FIRED,
            eventObj: {
                actionId: actionId,
                type: CvActionFiredResultType.ACTION_STARTED,
                source: source,
                clientAction: clientAction
            }
        };
        actionListeners.forEach((listener)=> {
            listener(e)
        });
        eventRegistry.publish<CvActionFiredResult>(e, false);
        return e;
    },

    /* static */
    /* intended for resuse by other components */
    _publishActionFinished: function (actionId:string, source:PaneContext,
                                      actionListeners:Array<(event:CvEvent<CvActionFiredResult>)=>void>,
                                      eventRegistry:CvEventRegistry):CvEvent<CvActionFiredResult> {
    
        const e:CvEvent<CvActionFiredResult> = {
            type: CvEventType.ACTION_FIRED,
            eventObj: {actionId: actionId, type: CvActionFiredResultType.ACTION_COMPLETED, source: source, clientAction:false}
        };
        actionListeners.forEach((listener)=> {
            listener(e)
        });
        eventRegistry.publish<CvActionFiredResult>(e, false);
        
        return e;
    },

    /** End static methods */

};

export interface CvActionState extends CvActionBaseState {
}

export interface CvActionProps extends CvActionBaseProps {
}

/*
 ***************************************************
 * A catavolt action
 ***************************************************
 */
export var CvAction = React.createClass<CvActionProps, CvActionState>({

    mixins: [CvBaseMixin, CvActionBase],

    componentDidMount: function() {
        this._componentDidMount();
    },

    componentWillReceiveProps(nextProps) {
        this._componentWillReceiveProps(nextProps);
    },

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

    getDefaultProps: function () {
        return {
            actionId: null,
            fireOnLoad:null,
            paneContext: null,
            navTarget: null,
            menuDef: null,
            actionListeners: [],
            navigationListeners: [],
            stateChangeListeners: [],
            selectionProvider: null,
            renderer: null,
            wrapperElemName: 'span',
            wrapperElemProps: {},
            wrapperEventHandlerName: 'onClick',
            actionHandlerProvider: null
            
        }
    },

    getInitialState: function() {
        return this._getInitialState();
    },

    render: function () {
        if (this.props.renderer) {
            return this.props.renderer(this.getChildContext().cvContext, this._getCallbackObj())
        } else if (this.props.children){
            const props = ObjUtil.addAllProps(this.props.wrapperElemProps, {});
            props[this.props.wrapperEventHandlerName] = ()=>{this.performAction()};
            return React.createElement(this.props.wrapperElemName, props, this.props.children);
        } else {
            return null;
        }
    }

});

