/*
 * Copyright 2004-2014 ICEsoft Technologies Canada Corp.
 *
 * Licensed 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.icefaces.ace.component.fileentry;

import org.icefaces.ace.util.JSONBuilder;
import org.icefaces.ace.util.Utils;
import org.icefaces.component.Focusable;
import org.icefaces.impl.event.BridgeSetup;
import org.icefaces.util.JavaScriptRunner;

import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.application.ProjectStage;
import javax.faces.context.FacesContext;
import javax.faces.event.FacesEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.AbortProcessingException;
import javax.faces.component.UIForm;
import javax.el.ELContext;
import javax.el.ELException;
import javax.el.MethodExpression;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

public class FileEntry extends FileEntryBase implements Focusable {
    private static Logger log = Logger.getLogger(FileEntry.class.getName());
    private static final String RESULTS_KEY = "org.icefaces.ace.component.fileEntry.results";
    private static final String EVENT_KEY = "org.icefaces.ace.component.fileEntry.events";

    public FileEntry() {
        super();
    }

    public void setResults(FileEntryResults results) {
        try {
            super.setResults(results);
        }
        catch(RuntimeException e) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            if (facesContext.isProjectStage(ProjectStage.Development) ||
                facesContext.isProjectStage(ProjectStage.UnitTest)) {
                log.log(Level.WARNING, "Problem setting results property on " +
                    "FileEntry component", e);
            }
            throw e;
        }
    }

    public void reset() {
        setResults(null);
    }

    public String getCallbackEL() {
        javax.el.ValueExpression callbackExpression = getValueExpression("callback");
        return callbackExpression == null ? null : callbackExpression.getExpressionString();
    }

    public boolean isViaCallback() {
        return getCallbackEL() != null;
    }

    public String getProgressResourceName(FacesContext context) {
        if (PushUtils.isPushPresent()) {
            UIForm form = Utils.findParentForm(this);
            return PushUtils.getProgressResourceName(context, form);
        } else {
            return null;
        }
    }

    public String getProgressGroupName(FacesContext context) {
        if (PushUtils.isPushPresent()) {
            UIForm form = Utils.findParentForm(this);
            return PushUtils.getPushGroupName(context, form);
        } else {
            return null;
        }
    }

    /**
     * Used by FileEntryPhaseListener, before the component treee exists, to 
     * save the results of the file uploads, to be retrieved later in the same 
     * lifecycle, once the component tree is in place. 
     */
    static void storeResultsForLaterInLifecycle(
            FacesContext facesContext,
            Map<String, FileEntryResults> clientId2Results) {
        facesContext.getAttributes().put(RESULTS_KEY, clientId2Results);
    }

    /**
     * Used by FileEntryRenderer.decode(-) to retrieve each fileEntry
     * component's results for file uploads.
     */
    static FileEntryResults retrieveResultsFromEarlierInLifecycle(
            FacesContext facesContext, String clientId) {
        FileEntryResults results = null;
        Map<String, FileEntryResults> clientId2Result =
            (Map<String, FileEntryResults>) facesContext.getAttributes().get(
                RESULTS_KEY);
        if (clientId2Result != null) {
            results = clientId2Result.get(clientId);
        }
        return results;
    }

    /**
     * After the ApplyRequestValues phase, when the fileEntry components 
     * have all retrieved their results for uploaded files, clear the 
     * results away, so we don't leak memory. 
     */
    static void removeResults(FacesContext facesContext) {
        facesContext.getAttributes().remove(RESULTS_KEY);
    }

    /**
     * Invoked by processDecodes(FacesContext) or processValidators(FacesContext)
     */
    protected void validateResults(FacesContext facesContext) {
        log.finer("FileEntry.validateResults()  clientId: " + getClientId(facesContext));
        // The current lifecycle results. If no files were uploaded
        // this lifecycle, then this is null. Different from getResults(),
        // which may be from a previous lifecycle.
        FileEntryResults results = retrieveResultsFromEarlierInLifecycle(
            facesContext, getClientId(facesContext));

        boolean failed = false;
        if (results != null) {
            for(FileEntryResults.FileInfo fi : results.getFiles()) {
                if (!fi.isSaved()) {
                    log.finer("FileEntry.validateResults()    FAILED  file: " + fi);
                    failed = true;
                    break;
                }
            }
        }
        else {
            // No files uploaded this lifecycle
            // If required then failed unless was partial submit
            String partialSubmitValue = facesContext.getExternalContext().
                    getRequestParameterMap().get("ice.submit.partial");
            boolean partialSubmit = "true".equals(partialSubmitValue);
            log.finer("FileEntry.validateResults()    partialSubmit: " + partialSubmit + "  required: " + isRequired());
            if (!partialSubmit && isRequired()) {
                if (isMessagePersistence()) {
                    addMessageFromRequired(facesContext);
                }
                failed = true;
                log.finer("FileEntry.validateResults()    FAILED  required");
            }
        }
        if (failed) {
            facesContext.validationFailed();
            facesContext.renderResponse();
        }
    }

    protected void addMessagesFromResults(FacesContext facesContext) {
        String clientId = getClientId(facesContext);
        FileEntryResults results = getResults();
        log.finer("FileEntry.addMessagesFromResults  clientId: " + clientId + "  results: " + results);
        if (results != null) {
            ArrayList<FileEntryResults.FileInfo> files = results.getFiles();
            for (FileEntryResults.FileInfo fi : files) {
                FileEntryStatus status = fi.getStatus();
                FacesMessage fm = status.getFacesMessage(facesContext, this, fi);
                log.finer(
                    "FileEntry.addMessagesFromResults\n" +
                    "  FileInfo: " + fi + "\n" +
                    "  FacesMessage: " + fm);
                facesContext.addMessage(clientId, fm);
            }
        }
    }

    protected void addMessageFromRequired(FacesContext facesContext) {
        String clientId = getClientId(facesContext);
        FacesMessage fm = FileEntryStatuses.REQUIRED.getFacesMessage(
                facesContext, this, null);
        log.finer("FileEntry.addMessageFromRequired  clientId: " + clientId + "  FacesMessage: " + fm);
        facesContext.addMessage(clientId, fm);

    }

    /**
     * @return The label property, if specified, else the clientId
     */
    public String getFacesMessageLabel() {
        String label = getLabel();
        if (label != null && label.length() > 0) {
            return label;
        }
        return getClientId();
    }

	private boolean isResetValidRequest(FacesContext facesContext) {
		java.util.Map<String, String> requestMap = facesContext.getExternalContext().getRequestParameterMap();
		return (requestMap.get(getClientId(facesContext)+"_resetValid") != null);
	}

    @Override
    /**
     * Override to add the constraint that when immediate is true, then
     * immediateValidation must be true as well, since validation must happen
     * before the fileEntryListener is invoked.
     * 
     * @see FileEntryBase#isImmediateValidation
     */
    public boolean isImmediateValidation() {
        return isImmediate() ? true : super.isImmediateValidation();
    }

    @Override
    public void processDecodes(FacesContext facesContext) {
		if (isResetValidRequest(facesContext)) return;
        super.processDecodes(facesContext);

        if (isImmediateValidation()) {
            validateResults(facesContext);
        }
    }

    @Override
    public void processValidators(FacesContext facesContext) {
		if (isResetValidRequest(facesContext)) return;
        super.processValidators(facesContext);

        if (!isImmediateValidation()) {
            validateResults(facesContext);
        }
    }

    @Override
    public void queueEvent(FacesEvent event) {
        log.finer("FileEntry.queueEvent  clientId: " + getClientId());
        if (isImmediate()) {
            event.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
            log.finer("FileEntry.queueEvent    immediate == true  queuing event: " + event);
            super.queueEvent(event);
        }
        else {
            event.setPhaseId(PhaseId.RENDER_RESPONSE);
            log.finer("FileEntry.queueEvent    immediate == false  storing event: " + event);
            storeEventForPreRender(event);
        }
    }
    
    @Override
    public void broadcast(FacesEvent event) {
        log.finer("FileEntry.broadcast  clientId: " + getClientId() + "  event: " + event + "  phaseId: " + event.getPhaseId());
        if (event instanceof FileEntryEvent) {
            FileEntryEvent fee = (FileEntryEvent) event;
            FacesContext context = FacesContext.getCurrentInstance();
            try {
                log.finer("FileEntry.broadcast    invoke: " + fee.isInvoke());
                if (fee.isInvoke()) {
                    MethodExpression listener = getFileEntryListener();
                    if (listener != null) {
                        ELContext elContext = context.getELContext();
                        try {
                            Object result = listener.invoke(elContext, new Object[] {event});
                            log.finer("FileEntry.broadcast    result: " + result);
                            if (result != null) {
                                String outcome = result.toString();
                                context.getApplication().getNavigationHandler().handleNavigation(
                                    context,
                                    (null != listener) ? listener.getExpressionString() : null,
                                    outcome);
                                context.renderResponse();
                                return;
                            }
                        } catch (ELException ee) {
                            throw new AbortProcessingException(ee.getMessage(),
                                    ee.getCause());
                        }
                    }

                    // If every file succeeded uploading, and the lifecycle is
                    // valid, then clear the file selection in the browser
                    if (getResults().isLifecycleAndUploadsSuccessful(context)){
                        String script = "ice.ace.fileentry.clearFileSelection(\"" +
                                JSONBuilder.escapeString(getClientId(context))+
                                "\")";
                        JavaScriptRunner.runScript(context, script);
                    }
                }
            } finally {
                // ICE-5750 deals with re-adding faces messages for components
                // that have not re-executed. Components that are executing
                // should re-add their faces messages themselves.
                // FileEntry will only re-add faces messages past the upload
                // lifecycle if its messagePersistence property is true.
                if (isMessagePersistence() || fee.isInvoke()) {
                    addMessagesFromResults(context);
                }
            }
        }
        else {
            super.broadcast(event);
        }
    }
    
    /**
     * Used by FileEntry.queueEvent(-), to save non-immediate FileEntryEvent
     * objects to be invoked by FileEntryPhaseListener in pre-Render phase. 
     */
    private void storeEventForPreRender(FacesEvent event) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        Map<String,FacesEvent> clientId2FacesEvent = (Map<String,FacesEvent>)
            facesContext.getAttributes().get(EVENT_KEY);
        if (clientId2FacesEvent == null) {
            clientId2FacesEvent = new HashMap<String,FacesEvent>(6);
            facesContext.getAttributes().put(EVENT_KEY, clientId2FacesEvent);
        }
        clientId2FacesEvent.put(getClientId(facesContext), event);
    }

    /**
     * Used by FileEntryPhaseListener(-) to retrieve each the FileEntryEvent
     * objects, so they can be invoked pre-Render phase.
     */
    static Map<String,FacesEvent> removeEventsForPreRender(
            FacesContext facesContext) {
        Map<String,FacesEvent> clientId2FacesEvent = (Map<String,FacesEvent>)
            facesContext.getAttributes().remove(EVENT_KEY);
        return clientId2FacesEvent;
    }

    public String getFocusedElementId() {
        return getClientId() + "::" + BridgeSetup.getViewID(FacesContext.getCurrentInstance().getExternalContext());
    }

}
