/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.empire.jsf2.pages;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.application.NavigationHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;

import org.apache.empire.commons.StringUtils;
import org.apache.empire.db.DBDatabase;
import org.apache.empire.db.DBRowSet;
import org.apache.empire.exceptions.EmpireException;
import org.apache.empire.exceptions.InternalException;
import org.apache.empire.exceptions.ItemNotFoundException;
import org.apache.empire.exceptions.MiscellaneousErrorException;
import org.apache.empire.jsf2.app.FacesUtils;
import org.apache.empire.jsf2.app.TextResolver;
import org.apache.empire.jsf2.app.WebApplication;
import org.apache.empire.jsf2.utils.ParameterMap;
import org.apache.empire.jsf2.utils.ParameterObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class Page implements Serializable
{
    private static final long   serialVersionUID = 1L;

    public static final String  SESSION_MESSAGE  = "PAGE_SESSION_MESSAGE";

    private static final Logger log              = LoggerFactory.getLogger(Page.class);

    private String              action           = null;
    private boolean             initialized      = false;
    private PageDefinition      pageDefinition   = null;
    private List<PageElement>   pageElements     = null;

    protected Page()
    {
        if (log.isDebugEnabled())
        {   String name = this.getClass().getSimpleName();
            Page.log.debug("PageBean {} created.", name);
        }
    }

    public String getPageName()
    {
        return (pageDefinition != null ? pageDefinition.getPageBeanName() : "{" + getClass().getSimpleName() + "}");
    }

    public String getName()
    {
        String className = pageDefinition.getPageBeanClass().getName();
        int lastDot = className.lastIndexOf(".");
        String name = className.substring(lastDot + 1);
        return name;
    }

    public boolean isInitialized()
    {
        return initialized;
    }

    public String getAction()
    {
        if (this.action==null)
            return null;

        // Generate key
        ParameterMap pm = FacesUtils.getParameterMap(FacesUtils.getContext());
        String actionParam = (pm!=null ? pm.encodeString(action) : action);
        return actionParam;
    }

    public void setAction(String actionParam)
    {
        if (!initialized)
            Page.log.debug("Setting PageBean action {} for bean {}.", action, getPageName());
        else
            Page.log.trace("Re-setting PageBeanAction {} for bean {}.", action, getPageName());
        
        // actionParam
        if (StringUtils.isEmpty(actionParam))
            return;

        // Set action from param
        this.action = PageDefinition.decodeActionParam(actionParam);
        if (this.action==null)
            throw new ItemNotFoundException(actionParam); 
    }

    public PageDefinition getPageDefinition()
    {
        return pageDefinition;
    }

    public void setPageDefinition(PageDefinition pageDefinition)
    {
        this.pageDefinition = pageDefinition;
    }

    public PageDefinition getParentPage()
    {
        return pageDefinition.getParent();
    }

    public void preRenderPage(FacesContext context)
    {
        if (this.initialized)
        {
            // PageBean.log.error("PageBean {} is already initialized.", name());
            try
            {
                Page.log.debug("PageBean {} is already initialized. Calling doRefresh().", getPageName());
                doRefresh();
            }
            catch (Exception e)
            {
                logAndHandleActionException("doRefresh", e);
            }
            return; // already Initialized
        }

        // Check access
        try
        {
            checkPageAccess();
            // redirected?
            if (context.getResponseComplete())
                return;
        }
        catch (Exception e)
        {
            logAndHandleActionException("checkAccess", e);
            // redirected?
            if (context.getResponseComplete())
                return;
            // Oops, not redirected yet?
            if (getParentPage()!=null)
                navigateTo(getParentOutcome(true)); 
            // Done
            return;
        }

        // Initialize
        this.initialized = true;

        // String value of "null"?
        if (this.action!=null && "null".equals(this.action))
        {   log.warn("Invalid action name 'null' for {}", getClass().getName());
            this.action = null;
        }    
        
        // Execute Action
        if (this.action != null)
        {   try
            {   // Process action
                log.info("Processing action {} on {}.", String.valueOf(action), getPageName());
                Method method = getClass().getMethod(action);
                Object result = method.invoke(this);
                if (result != null)
                {
                    String outcome = result.toString();
                    // Retrieve the NavigationHandler instance..
                    NavigationHandler navHandler = context.getApplication().getNavigationHandler();
                    // Invoke nav handling..
                    navHandler.handleNavigation(context, action, outcome);
                    // Trigger a switch to Render Response if needed
                    context.renderResponse();
                    return;
                }
                restoreSessionMessage();
            }
            catch (NoSuchMethodException nsme)
            {
                logAndHandleActionException(action, nsme);
            }
            catch (Exception e)
            {
                logAndHandleActionException(action, e.getCause());
            }
            finally
            {
                // Clear action
                this.action = null; // Page.INVALID_ACTION;
            }
        }
        else
        {   // call default Action
            try
            {
                Page.log.debug("Initializing PageBean {}. Calling doInit()", getPageName());
                doInit();
                restoreSessionMessage();
            }
            catch (Exception e)
            {
                logAndHandleActionException("doInit", e);
            }
        }
    }
    
    public boolean isHasMessages()
    {
        Iterator<FacesMessage> fmi = FacesContext.getCurrentInstance().getMessages();
        return fmi.hasNext();
    }
    
    protected void checkPageAccess()
    {
        /* Throw exception if User has no Access */
    }

    private void restoreSessionMessage()
    {
        // Restore Session Error Message
        ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext();
        Map<String, Object> map = ec.getSessionMap();
        if (map.containsKey(SESSION_MESSAGE))
        {
            FacesMessage errorMsg = (FacesMessage) map.get(SESSION_MESSAGE);
            FacesContext.getCurrentInstance().addMessage(getPageName(), errorMsg);
            map.remove(SESSION_MESSAGE);
        }
    }

    private void logAndHandleActionException(String action, Throwable e)
    {
        String msg = "Failed to perform action " + action + " on " + getPageName();
        // Message
        Page.log.error(msg, e);
        if (!handleActionError(action, e))
        {   // Not handled. Throw again
            if (e instanceof EmpireException)
                throw ((EmpireException)e);
            else
                throw new InternalException(e);
        }    
    }
    
    protected void setSessionMessage(FacesMessage facesMsg)
    {
        // Set Session Message
        ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext();
        ec.getSessionMap().put(SESSION_MESSAGE, facesMsg);
    }
    
    protected void setSessionError(Throwable e)
    {
        // Set Session Message
        String msg = getErrorMessage(e);
        String detail = extractErrorMessageDetail(action, e, 1);
        if (log.isDebugEnabled())
            log.debug(msg + "\r\n" + detail, e);
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, detail);
        setSessionMessage(facesMsg);
    }

    protected boolean handleActionError(String action, Throwable e)
    {
        // Set Faces Message
        String msg = getErrorMessage(e);
        String detail = extractErrorMessageDetail(action, e, 1);
        log.error(msg + "\r\n" + detail);
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, detail);
        setSessionMessage(facesMsg);
        // Return to parent page
        PageDefinition parentPage = getParentPage();
        if (parentPage == null)
        {   FacesContext.getCurrentInstance().addMessage(getPageName(), facesMsg);
            return false;
        }
        // redirect
        navigateTo(parentPage.getRedirect());
        return true;
    }

    protected void setErrorMessage(Throwable e)
    {
        String msg = getErrorMessage(e);
        String detail = extractErrorMessageDetail(action, e, 1);
        if (log.isDebugEnabled())
            log.debug(msg + "\r\n" + detail, e);
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, detail);
        FacesContext.getCurrentInstance().addMessage(getPageName(), facesMsg);
    }

    protected String getErrorMessage(Throwable e)
    {   // Wrap Exception
        if (!(e instanceof EmpireException))
            e = new InternalException(e);
        // get message 
        return getTextResolver().getExceptionMessage((Exception)e);
    }
    
    protected String extractErrorMessageDetail(String action, Throwable e, int stackTraceElements)
    {
        StringBuilder b = new StringBuilder();
        b.append("Error performing action '");
        b.append(action);
        b.append("' on page ");
        b.append(getPageName());
        b.append(": ");
        b.append(e.toString());
        b.append("\r\nat:");
        StackTraceElement[] stack = e.getStackTrace();
        int len = (stack.length>stackTraceElements) ? stackTraceElements : stack.length; 
        for (int i=0; i<len; i++)
        {
            b.append(stack[i].toString());
            b.append("\r\n");
        }
        return b.toString();
    }

    /**
     * navigates to the desired page. Depending on the page outcome provided this is either a forward or a redirect.
     * @param outcome the destination page to navigate to
     */
    protected void navigateTo(PageOutcome outcome)
    {
        if (log.isDebugEnabled())
            log.debug("Redirecting from page {} to page {}.", getPageName(), outcome.toString());
        // Return to Parent
        FacesContext context = FacesContext.getCurrentInstance();
        // Retrieve the NavigationHandler instance..
        NavigationHandler navHandler = context.getApplication().getNavigationHandler();
        // Invoke nav handling..
        navHandler.handleNavigation(context, action, outcome.toString());
        // Trigger a switch to Render Response if needed
        context.renderResponse();
    }

    /**
     * adds a page element to this page
     * DO NOT CALL yourself, this method is called from the PageElement constructor!
     * 
     * @param element
     */
    protected void registerPageElement(PageElement element)
    {
        if (pageElements == null)
            pageElements = new ArrayList<PageElement>(1);
        // register now
        if (pageElements.contains(element) == false)
            pageElements.add(element);
        else
            log.warn("PageElement {} was registered twice!", element.getPropertyName());
    }

    /**
     * Helper methods for parent outcome
     * 
     * @param action
     * @param redirect
     * @return the parent outcome string
     */
    protected PageOutcome getParentOutcome(String action, boolean redirect)
    {
        PageDefinition parentPage = getParentPage();
        if (parentPage == null)
            throw new MiscellaneousErrorException("No Parent Page defined for " + getPageName());
        if (redirect)
            return parentPage.getRedirect(action);
        else
            return parentPage.getOutcome(action);
    }

    protected PageOutcome getParentOutcome(boolean redirect)
    {
        return getParentOutcome(null, redirect);
    }

    /**
     * return a connection for a particular database
     * @param db the database for which to obtain a connection
     * @return the connection for the given database
     */
    public Connection getConnection(DBDatabase db)
    {       
        WebApplication app = FacesUtils.getWebApplication();
        return app.getConnectionForRequest(FacesUtils.getContext(), db);
    }

    public <T extends ParameterObject> T getObjectFromParam(Class<T> paramType, String idParam)
    {
        FacesContext fc = FacesUtils.getContext();
        ParameterMap paramMap = FacesUtils.getParameterMap(fc);
        return paramMap.get(paramType, idParam);
    }

    public Object[] getKeyFromParam(DBRowSet rowset, String idParam)
    {
        FacesContext fc = FacesUtils.getContext();
        return FacesUtils.getParameterMap(fc).getKey(rowset, idParam);
    }
    
    public Object[] getKeyFromParam(PageDefinition page, DBRowSet rowset, String idParam)
    {
        FacesContext fc = FacesUtils.getContext();
        return FacesUtils.getParameterMap(fc).getKey(page, rowset, idParam);
    }
    
    public String getIdParamForKey(DBRowSet rowset, Object[] key)
    {
        FacesContext fc = FacesUtils.getContext();
        return FacesUtils.getParameterMap(fc).put(rowset, key);
    }

    public String getIdParamForKey(PageDefinition page, DBRowSet rowset, Object[] key)
    {
        FacesContext fc = FacesUtils.getContext();
        return FacesUtils.getParameterMap(fc).put(page, rowset, key);
    }
    
    public void addJavascriptCall(String function)
    {
        // Add Call
        FacesContext fc = FacesUtils.getContext();
        WebApplication app = FacesUtils.getWebApplication();
        app.addJavascriptCall(fc, function);
    }
    
    /* Page Resources */
    
    /**
     * Adds an object required for resource handling, to the page resource map.
     * <pre>
     * Since resource requests are not attached to a view, they cannot access page properties via expression language like this
     *    #{page.someProperty}
     * Instead, the page should add properties that are required to the "pageResources"-map. This map is held on the session, and cleared when the page changes.
     * In order to access such page resources via expression language use
     *    #{pageResources.someProperty}
     * </pre>   
     * @param name the name of the resource
     * @param resource the resource
     */
    public void addPageResource(String name, Object resource)
    {
        Map<String, Object> prm = FacesUtils.getPageResourceMap(FacesUtils.getContext());
        prm.put(name, this);
    }
    
    /**
     * Returns the page resource object previously added by addPageResource(...)
     * @param name the name of the resource
     * @return resource the resource
     */
    public Object getPageResource(String name)
    {
        Map<String, Object> prm = FacesUtils.getPageResourceMap(FacesUtils.getContext());
        return prm.get(name);
    }
    
    /* Default Init Method */
    public void doInit()
    {
        if (pageElements != null)
        {   // Init Page Elements
            for (PageElement pe : pageElements)
                doInitElement(pe);
        }
    }

    public void doRefresh()
    {
        if (pageElements != null)
        {   // Refresh Page Elements
            for (PageElement pe : pageElements)
                doRefreshElement(pe);
        }
    }
    
    /**
     * called by doInit() to initialize a particular page element
     * @param pe the page element to initialize
     */
    protected void doInitElement(PageElement pe)
    {
        pe.onInitPage();
    }
    
    /**
     * called by doRefresh() to refresh a particular page element
     * @param pe the page element to refresh
     */
    protected void doRefreshElement(PageElement pe)
    {
        pe.onRefreshPage();
    }
    
    /* Helpers */
    
    protected final TextResolver getTextResolver()
    {
        FacesContext fc = FacesUtils.getContext();
        return FacesUtils.getTextResolver(fc);
    }
}
