/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */


/**
 * Diese Klasse ist für die Verabeitung der URLs innerhalb der Anwendung navigation zuständig.
 */
package com.arsdigita.london.navigation;


import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;

import com.arsdigita.categorization.Category;
import com.arsdigita.categorization.CategoryCollection;
import com.arsdigita.cms.TemplateContext;
import com.arsdigita.domain.DomainCollection;
import com.arsdigita.london.terms.Domain;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.kernel.Kernel;
import com.arsdigita.util.Assert;
import com.arsdigita.util.StringUtils;
import com.arsdigita.web.Application;
import com.arsdigita.web.DefaultApplicationFileResolver;
import com.arsdigita.web.Web;

public class NavigationFileResolver extends DefaultApplicationFileResolver {

    private static final Logger s_log = 
        Logger.getLogger(NavigationFileResolver.class);

    private static final String CATEGORY_PATH_ATTR = 
        NavigationFileResolver.class + ".categoryPath";

	// path is set in a cookie, which navigation models may use if they wish
	public static final String PATH_COOKIE_NAME = "ad_path";
	public static final char PATH_COOKIE_SEPARATOR = '|';
	
    public RequestDispatcher resolve(String templatePath,
                                     HttpServletRequest sreq,
                                     HttpServletResponse sresp,
                                     Application app) {
        String path = sreq.getPathInfo();
        if (s_log.isDebugEnabled()) {
            s_log.debug("Resolving " + path);
        }

        if (path.equals("/category.jsp")) {
            Navigation nav = (Navigation)Web.getContext().getApplication();
            
            String id = sreq.getParameter("categoryID");
            if (s_log.isDebugEnabled()) {
                s_log.debug("dispatching to category " + id);
            }

            String useContext = sreq.getParameter("useContext");
            if (s_log.isDebugEnabled()) {
                s_log.debug("Using use context " + useContext );
            }
            if ( null == useContext ) {
                useContext = Template.DEFAULT_USE_CONTEXT;
            }
            Category cat = null;

            // if the category doesn't exist, send a 404, nicer
            try {
                cat = new Category(new BigDecimal(id));
            } catch (Exception e) {
                s_log.warn("Could not load category for id "+id);
                return null;
            }

            // check that the category is enabled, otherwise 404
            if (!cat.isEnabled()) {
                s_log.warn("Category is disabled.");
                return null;
            }

            // check that the category is in the tree of categories
            Category root = null;
            DataCollection objs = SessionManager.getSession()
                .retrieve(Domain.BASE_DATA_OBJECT_TYPE);
            objs.addEqualsFilter("model.ownerUseContext.categoryOwner.id", nav.getID());
            String dispatcherContext = null;
            TemplateContext tc = Navigation.getContext().getTemplateContext();
            if (tc != null) {
                dispatcherContext = tc.getContext();
            }
            objs.addEqualsFilter("model.ownerUseContext.useContext", dispatcherContext);
            DomainCollection domains = new DomainCollection(objs);
            if (domains.next()) {
                root = ((Domain) domains.getDomainObject()).getModel();
            } else {
                // can't find domain, 404
                s_log.warn("Category domain does not exist.");
                return null;
            }
            domains.close();
            if (root == null) {
                // no root category, 404
                s_log.warn("Category domain does not have a root category.");
                return null;
            }

            if (!root.isMemberOfSubtree(cat)) {
                // it is not in the tree, send 404
                s_log.warn("Category doesn't belong to navigation tree.");
                return null;
            }

            CategoryCollection parents = cat.getDefaultAscendants();
            parents.addOrder(Category.DEFAULT_ANCESTORS);
            List cats = new ArrayList();
            while (parents.next()) {
                cats.add(parents.getCategory());
            }

            Category[] catsArray = (Category[])
                cats.toArray(new Category[cats.size()]);
            
            sreq.setAttribute(CATEGORY_PATH_ATTR,
                              catsArray);
            setPathCookie(sresp, catsArray);
            return resolveTemplate(catsArray[catsArray.length-1], useContext);
        } else {
            String useContext = Template.DEFAULT_USE_CONTEXT;
            if (path.endsWith( ".jsp" )) {
                int lastSlash = path.lastIndexOf( '/' );

                useContext = path.substring( lastSlash + 1, path.length() - 4 );
                path = path.substring( 0, lastSlash );

                if (s_log.isDebugEnabled()) {
                    s_log.debug( "useContext=" + useContext + ",path=" + path );
                }
            }

            Category root = getRootCategory();
            Category[] cats = resolvePath(root, path);
            
            if (cats == null || cats.length == 0) {
                if (s_log.isDebugEnabled()) {
                    s_log.debug("No category found");
                }
                sreq.setAttribute(CATEGORY_PATH_ATTR,
                                  new Category[] { root });
				setPathCookie(sresp, new Category[] { root });
                return super.resolve(templatePath, sreq, sresp, app);
            } else {
                Category cat = cats[cats.length-1];
                if (s_log.isDebugEnabled()) {
                    s_log.debug("Got cat " + cat);
                }
				setPathCookie(sresp, cats);
                sreq.setAttribute(CATEGORY_PATH_ATTR,
                                  cats);
                RequestDispatcher rd = resolveTemplate(cat, useContext);
                if (rd == null) {
                    return super.resolve(templatePath, sreq, sresp, app);
                }
                return rd;
            }
        }
    }
        
    /**
     * @param catsArray
     * sets current path in a cookie
     */
    private void setPathCookie(HttpServletResponse resp, Category[] catsArray) {
		
	// 1st part of cookie value is website - if cookie domain covers several Aplaws sites, 
	// and the navigation model retains the cookie for more than one request, 
	// we could potentially link to one site with a path relating to another site. A check on this part
	// of the cookie will prevent problems
	StringBuffer path = new StringBuffer(Web.getConfig().getSiteName());
		
	// 2nd part of cookie value is the application that set it. Again may be used if a navigation model
	// retains the cookie. If we link to another application, it's navigation model may 
	// use this when deciding whether to trust the given path
		
	path.append(PATH_COOKIE_SEPARATOR + Kernel.getContext().getResource().getID().toString());
	for (int i = 0; i < catsArray.length; i ++) {
	    Category cat = catsArray[i];
	    path.append(PATH_COOKIE_SEPARATOR + cat.getID().toString());
	}
	Cookie cookie = new Cookie(PATH_COOKIE_NAME, path.toString());
	s_log.debug("setting cookie with value: " + path);
	cookie.setMaxAge(-1);
	cookie.setPath("/");
	String domain = Kernel.getSecurityConfig().getCookieDomain();
	if (domain != null) {
	    cookie.setDomain(domain);
	}
	resp.addCookie(cookie);
		
		
    }
	
	
	
	

    public static Category[] getCategoryPath(HttpServletRequest req) {
        return (Category[])req.getAttribute(CATEGORY_PATH_ATTR);
    }
    
    private String getTemplateContext() {
        TemplateContext ctx = Navigation.getContext().getTemplateContext();
        if (ctx == null) {
            return Template.DEFAULT_DISPATCHER_CONTEXT;
        } else {
            return ctx.getContext();
        }
    }

    private RequestDispatcher resolveTemplate(Category cat, String useContext) {
	Template template = null;
        if (Navigation.getConfig().inheritTemplates()) {
	    template = Template.matchBest(cat,
            getTemplateContext(),
	    useContext);
        } else {
            template = Template.matchExact(cat,
            getTemplateContext(),
            useContext);
        }
        // If there's an explicit use context which doesn't exist, give a 404
        if (!Template.DEFAULT_USE_CONTEXT.equals( useContext ) && 
            null == template) {
            s_log.debug("No template found in context " + 
                        getTemplateContext() + " for category " +
                        cat.getID() + " with use context " + 
                        useContext );
            return null;
        }
        
        String path = null;
        if (template == null) {
            if (s_log.isDebugEnabled()) {
                s_log.debug("Using default template");
            }
            path = Navigation.getConfig().getDefaultTemplate();
        } else {
            path = template.getURL();
        }
        RequestDispatcher rd = Web.findResourceDispatcher(
            new String[] { "ROOT"} ,
            path);
            
        if (s_log.isDebugEnabled()) {
            s_log.debug("Got dispatcher " + rd);
        } 
        
        return rd;
    }

    private Category getRootCategory() {
        Navigation nav = (Navigation)Web.getContext().getApplication();
        TemplateContext ctx = Navigation.getContext().getTemplateContext();
        
        if (s_log.isDebugEnabled()) {
            s_log.debug("Finding root for " + nav + " in context " + ctx);
        }

        String dispatcherContext = ctx == null ? null : ctx.getContext();

        Category root = Category.getRootForObject(nav,
                                                  dispatcherContext);
        
        if (root == null && dispatcherContext != null) {
            s_log.debug("No specific root found, trying generic context");
            root = Category.getRootForObject(nav, null);
        }
        
        Assert.exists(root, Category.class);
        if (s_log.isDebugEnabled()) {
            s_log.debug("Got root category " + root);
        }
        return root;
    }


    /**
     * 
     * category resolution retained as an instance method to 
     * allow it to be overridden. Default functionality 
     * contained in static resolveCategory method
     * @param root
     * @param path
     * @return
     */
    protected Category[] resolvePath (Category root, String path) {
	return NavigationFileResolver.resolveCategory(root, path);	
    }
	

    /**
     * In dieser Methode wird eine URL mit dem Kategorienbaum abgeglichen
     * und die angeforderte Kategorie zurückgegeben, soweit sie vorhanden
     * ist.
     * 
     * Ursprünglich hat diese Methode addEqualsFilter verwendet, um die 
     * passenden Kategorien direkt in der SQL-Abfrage zu filtern. Das ist
     * aber mit den lokalisierten URL der neuen, lokalisierten Kategorien
     * nicht mehr möglich - oder zumindest habe ich keinen Weg gefunden.
     * Stattdessen wird die Filterung nun in Java vorgenommen.
     *
     * Quasimodo
     */
    public static Category[] resolveCategory(Category root,
                                       String path) {
        String[] bits = StringUtils.split(path, '/');
                
        List cats = new ArrayList();
        cats.add(root);

        Category cat = root;
        for (int i = 0 ; i < bits.length ; i++) {
            if ("".equals(bits[i])) {
                continue;
            }
            if (s_log.isDebugEnabled()) {
                s_log.debug("Looking for match to " + bits[i]);
            }

            CategoryCollection children = cat.getChildren();
//            children.addEqualsFilter(Category.URL, bits[i]);
//            children.addEqualsFilter(Category.IS_ENABLED, Boolean.TRUE);
//            if (children.next()) {
            boolean found = false;
            while (children.next()) {
                cat = children.getCategory();
                if(cat.getURL().equals(bits[i]) && cat.isEnabled() == true) {
                    if (s_log.isDebugEnabled()) {
                        s_log.debug("Got category " + cat);
                    }
                    cats.add(cat);
                    children.close();
                    found = true;
                    break;
                }
            }
//            } else {
            if(found == false) {
                if (s_log.isDebugEnabled()) {
                    s_log.debug("No category found ");
                }
                return null;
            }
        }
        
        return (Category[])cats.toArray(new Category[cats.size()]);
    }
}
