/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.webdevsupport;

import com.arsdigita.bebop.ActionLink;
import com.arsdigita.bebop.BebopMapDispatcher;
import com.arsdigita.bebop.BoxPanel;
import com.arsdigita.bebop.ColumnPanel;
import com.arsdigita.bebop.Component;
import com.arsdigita.bebop.Container;
import com.arsdigita.bebop.ControlLink;
import com.arsdigita.bebop.Label;
import com.arsdigita.bebop.Link;
import com.arsdigita.bebop.ListPanel;
import com.arsdigita.bebop.Page;
import com.arsdigita.bebop.PageFactory;
import com.arsdigita.bebop.PageState;
import com.arsdigita.bebop.RequestLocal;
import com.arsdigita.bebop.SimpleContainer;
import com.arsdigita.bebop.Table;
import com.arsdigita.bebop.event.ActionEvent;
import com.arsdigita.bebop.event.ActionListener;
import com.arsdigita.bebop.event.TableActionAdapter;
import com.arsdigita.bebop.event.TableActionEvent;
import com.arsdigita.bebop.parameters.IntegerParameter;
import com.arsdigita.bebop.parameters.ParameterModel;
import com.arsdigita.bebop.table.AbstractTableModelBuilder;
import com.arsdigita.bebop.table.DefaultTableCellRenderer;
import com.arsdigita.bebop.table.TableCellRenderer;
import com.arsdigita.bebop.table.TableModel;
import com.arsdigita.bebop.table.TableModelBuilder;
import com.arsdigita.db.DbHelper;
import com.arsdigita.developersupport.DeveloperSupport;
import com.arsdigita.dispatcher.AccessDeniedException;
import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.dispatcher.RequestContext;
import com.arsdigita.kernel.Kernel;
import com.arsdigita.kernel.Party;
import com.arsdigita.kernel.SiteNode;
import com.arsdigita.kernel.permissions.PermissionDescriptor;
import com.arsdigita.kernel.permissions.PermissionService;
import com.arsdigita.kernel.permissions.PrivilegeDescriptor;
import com.arsdigita.sitenode.SiteNodeRequestContext;
import com.arsdigita.util.StringUtils;
import com.arsdigita.web.LoginSignal;
import com.arsdigita.web.ParameterMap;
import com.arsdigita.web.RedirectSignal;
import com.arsdigita.web.URL;
import com.arsdigita.webdevsupport.log4j.CategoryPanel;
import com.arsdigita.webdevsupport.config.ConfigList;
import com.arsdigita.xml.Element;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;

/**
 * Dispatcher for Web Developer Support
 *  <p>
 *
 *  </p>
 *
 * @author Joseph A. Bank (jbank@alum.mit.edu)
 * @version 1.0
 **/
public class Dispatcher extends BebopMapDispatcher {
    private static final Logger s_log =
        Logger.getLogger(Dispatcher.class.getName());

    public static final String APP_NAME = "ds";

    public static final String versionId = "$Id: //core-platform/dev/src/com/arsdigita/webdevsupport/Dispatcher.java#29 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

    private static boolean s_showDSPages = false;

    private final static String CACHE_TABLE_URL = "cache-table";

    public Dispatcher() {
        super();
        Map m = new HashMap();

        /** The URLs the package will support.
         */
        m.put("", buildIndexPage());
        m.put("index",  buildIndexPage());
        m.put("request-info",  buildInfoPage());
        m.put("query-log",  new QueryLog());
        m.put("query-info",  buildQueryInfoPage());
        m.put("query-plan",  buildQueryPlanPage());
        m.put("log4j", buildLog4jPage());
        m.put("config", buildConfigPage());
        m.put(CACHE_TABLE_URL, buildCacheTablePage());

        /** The rest of the URLs go here with more
         *  m_put statements.
         */
        setMap(m);
    }

    public void dispatch(HttpServletRequest req,
                         HttpServletResponse resp,
                         RequestContext ctx)
        throws IOException, ServletException {
        Party party = Kernel.getContext().getParty();
        if (party == null) {
            throw new LoginSignal(req);
        }
        SiteNode node = ((SiteNodeRequestContext)ctx).getSiteNode();
        PermissionDescriptor admin = new PermissionDescriptor
            (PrivilegeDescriptor.ADMIN, node, party);
        if (!PermissionService.checkPermission(admin)) {
            throw new AccessDeniedException("user not an administrator");
        }

        // Want ds to always show the latest stuff...
        DispatcherHelper.cacheDisable(resp);

        DeveloperSupport.requestAddProperty(null, "IS_DS", new Boolean(true));
        super.dispatch(req, resp, ctx);
    }


    private Page buildIndexPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Web Developer Support");

        BoxPanel links = new BoxPanel(BoxPanel.VERTICAL);

        links.add(new Link("Log4j Logger Adjuster", "log4j"));
        links.add(new Link("Config Browser", "config"));
        links.add(new Link("Cache Table Browser", CACHE_TABLE_URL));
        
        ActionLink enable = new ActionLink("Enable request logging") {
                public boolean isVisible(PageState state) {
                    return !DeveloperSupport.containsListener(WebDevSupport.getInstance())
                        && super.isVisible(state);
                }
            };
        enable.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    DeveloperSupport.addListener(WebDevSupport.getInstance());
                    throw new RedirectSignal(URL.request(e.getPageState().getRequest(),
                                                         null), true);
                }
                
            });
        links.add(enable);


        ActionLink disable = new ActionLink("Disable request logging") {
                public boolean isVisible(PageState state) {
                    return DeveloperSupport.containsListener(WebDevSupport.getInstance())
                        && super.isVisible(state);
                }
            };
        disable.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    DeveloperSupport.removeListener(WebDevSupport.getInstance());
                    WebDevSupport.getInstance().clearRequestHistory();
                    throw new RedirectSignal(URL.request(e.getPageState().getRequest(),
                                                         null), true);
                }
            });
        links.add(disable);
        
        BoxPanel logs = new BoxPanel(BoxPanel.VERTICAL) {
                public boolean isVisible(PageState state) {
                    return DeveloperSupport.containsListener(WebDevSupport.getInstance())
                        && super.isVisible(state);
                }
            };

        logs.add(new Label("") {
                public String getLabel(PageState ps) {
                    return "Currently storing the last " +
                        WebDevSupport.getInstance().getMaxRequests() + " requests";
                }
            });
        Label toggle = new Label("") {
                public String getLabel(PageState ps) {
                    return s_showDSPages ? "Hide hits to developer support" :
                        "Show hits to developer support";
                }
            };
        ControlLink cl = new ControlLink(toggle) {
                public void respond(PageState s) {
                    s_showDSPages = !s_showDSPages;
                }
                public void setControlEvent(PageState s) {
                    s.setControlEvent(this);
                }
            };
        

        logs.add(cl);
        logs.add(new Label("<h3>Available Request Information</h3>", false));
        logs.add(makeRequestTable());
        
        p.add(links);
        p.add(logs);
        p.lock();
        return p;
    }

    private Page buildLog4jPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Log4j Logger Adjuster");
        p.add(new CategoryPanel());
        p.lock();
        return p;
    }

    private Page buildConfigPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Registry Config");
        p.add(new ConfigList());
        p.lock();
        return p;
    }

    private Page buildCacheTablePage() {
        Page p = PageFactory.buildPage(APP_NAME, "Cache Table Browser");
        p.add(new CacheTableBrowser());
        p.lock();
        return p;
    }

    private ParameterModel m_request_id = new IntegerParameter("request_id");
    private Page buildInfoPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Request Information");
        p.addGlobalStateParam(m_request_id);
        p.add(new RequestInfoComponent());
        p.add(makeDatabaseRequestComponent());
        p.lock();
        return p;
    }

    private ParameterModel m_query_id = new IntegerParameter("query_id");
    private ParameterModel m_query_request_id = new IntegerParameter("request_id");
    private Page buildQueryInfoPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Query Information");
        p.addGlobalStateParam(m_query_request_id);
        p.addGlobalStateParam(m_query_id);
        p.add(new QueryInfoComponent());
        p.lock();
        return p;
    }

    private RequestLocal m_scoreboard = new RequestLocal() {
            protected Object initialValue(PageState state) {
                // queryInfo.getID() => {textRepeats, queryRepeats}
                return new HashMap();
            }
        };

    // Used below in makeDatabaseRequestComponent to generate a query
    // scoreboard.
    private void incrementScore(final PageState state, final QueryInfo key,
                                final int index) {
        Map map = (Map) m_scoreboard.get(state);

        Integer[] row = (Integer[]) map.get(new Integer(key.getID()));

        if (row == null) {
            row = new Integer[] { new Integer(0), new Integer(0) };
            map.put(new Integer(key.getID()), row);
        }

        row[index] = new Integer(row[index].intValue() + 1);
    }

    private Component makeDatabaseRequestComponent() {
        final String[] headers =
            { "ID", "Duration", "Conn", "Command", "Exception" };

        TableModelBuilder b = new AbstractTableModelBuilder() {
                public TableModel makeModel(Table t, final PageState s) {
                    Integer request_id = (Integer)s.getValue(m_request_id);
                    RequestInfo ri =
                        WebDevSupport.getInstance().getRequest(request_id.intValue());
                    final Iterator iter = (ri == null) ? new ArrayList().iterator() :
                        ri.getQueries();

                    return new TableModel() {
                            private HashMap seenQueryAndValues = new HashMap();
                            private HashMap seenQuery = new HashMap();
                            private boolean duplicateQueryAndValues = false;
                            private boolean duplicateQuery = false;
                            private QueryInfo current = null;

                            public int getColumnCount() {
                                return 5;
                            }

                            public boolean nextRow() {
                                if (iter.hasNext()) {
                                    current = (QueryInfo) iter.next();

                                    duplicateQueryAndValues =
                                        seenQueryAndValues.containsKey(current);
                                    duplicateQuery =
                                        seenQuery.containsKey(current.getQuery());

                                    if (!duplicateQueryAndValues) {
                                        seenQueryAndValues.put(current, current);
                                    }

                                    if (!duplicateQuery) {
                                        seenQuery.put(current.getQuery(), current);
                                    }

                                    incrementScore
                                        (s, (QueryInfo) seenQueryAndValues.get(current),
                                         0);
                                    incrementScore
                                        (s, (QueryInfo) seenQuery.get(current.getQuery()),
                                         1);

                                    return true;
                                } else {
                                    return false;
                                }
                            }

                            public Object getElementAt(int columnIndex) {
                                switch (columnIndex) {
                                case 0: {
                                    StringBuffer result = new StringBuffer();
                                    result.append(current.getID());

                                    if (duplicateQueryAndValues) {
                                        result.append(
                                                      " duplicates #" +
                                                      ((QueryInfo)seenQueryAndValues.get(current)).getID()
                                                      );
                                    }

                                    if (duplicateQuery) {
                                        result.append(
                                                      " duplicates text of #" +
                                                      ((QueryInfo)seenQuery.get(current.getQuery())).getID()
                                                      );
                                    }

                                    return result.toString();
                                }
                                case 1: return current.getTime() + " ms";
                                case 2: return current.getConnectionID() + "";
                                case 3: return "<blockquote><pre>" +
                                            current.getQuery() +
                                            "</pre><br>BINDS: " +
                                            current.getBindvars() +
                                            "</blockquote>";
                                case 4: return current.getSQLE();
                                default: return null;
                                }
                            }

                            public Object getKeyAt(int columnIndex) {
                                return new Integer(current.getID());
                            }
                        };
                }
            };

        Table table = new Table(b, headers);
        table.setBorder("1");
        table.getColumn(3).setCellRenderer(new NonEscapedTableCellRenderer());
        table.getColumn(0).setCellRenderer(new
                                           DefaultTableCellRenderer(true));
        table.addTableActionListener(new TableActionAdapter() {
                public void cellSelected(TableActionEvent e) {
                    PageState s = e.getPageState();
                    Integer request_id = (Integer) s.getValue(m_query_request_id);

                    final ParameterMap params = new ParameterMap();

                    params.setParameter("request_id", request_id);
                    params.setParameter("query_id", e.getRowKey());
                    
                    if (e.getColumn().intValue() == 0) {
                        throw new RedirectSignal(URL.getDispatcherPath() + "/ds/query-info" + params, true);
                    } else {
                        throw new RedirectSignal(URL.getDispatcherPath() + "/ds/query-log" + params, true);
                    }
                }
            });

        Label l = new Label("None");
        l.setFontWeight(Label.ITALIC);
        l.setStyleAttr("padding-left: 3em");
        table.setEmptyView(l);
        table.setWidth("100%");

        BoxPanel panel = new BoxPanel(BoxPanel.VERTICAL);
        panel.add(table);

        TableModelBuilder scoreBuilder = new AbstractTableModelBuilder() {
                public TableModel makeModel(Table t, final PageState state) {
                    return new TableModel() {
                            Map map = (Map) m_scoreboard.get(state);
                            Iterator iter = map.keySet().iterator();
                            Integer key;
                            Integer[] row;

                            public int getColumnCount() {
                                return 3;
                            }

                            public boolean nextRow() {
                                if (iter.hasNext()) {
                                    key = (Integer) iter.next();
                                    row = (Integer[]) map.get(key);
                                    return true;
                                } else {
                                    return false;
                                }
                            }

                            public Object getElementAt(int column) {
                                switch (column) {
                                case 0:
                                    return key;
                                case 1:
                                    return row[0];
                                case 2:
                                    return row[1];
                                default:
                                    return null;
                                }
                            }

                            public Object getKeyAt(int column) {
                                return key;
                            }
                        };
                }
            };

        final String[] scoreHeaders = {"ID", "Duplicates", "Text-Duplicates"};

        Table scoreTable = new Table(scoreBuilder, scoreHeaders);
        panel.add(scoreTable);

        return panel;
    }


    private Table makeRequestTable() {
        final String[] headers = { "Time", "Duration", "Queries", "IP", "Request", "Extra" };

        TableModelBuilder b = new AbstractTableModelBuilder() {
                public TableModel makeModel(Table t, PageState s) {
                    return new TableModel() {
                            ListIterator iter =
                                WebDevSupport.getInstance().getRequestsReverse();
                            private RequestInfo current = null;

                            public int getColumnCount() {
                                return 6;
                            }

                            public boolean nextRow() {
                                while (iter.hasPrevious()) {
                                    current = (RequestInfo) iter.previous();
                                    boolean isdevsupp = current.isDevSupportRequest();
                                    if (s_showDSPages || !isdevsupp) {
                                        return true;
                                    }
                                }
                                return false;
                            }
                            static final int MAXSTR = 35;
                            public Object getElementAt(int columnIndex) {
                                switch (columnIndex) {
                                case 0: return current.getTime();
                                case 1: return current.getDuration();
                                case 2: return current.getNumQueries()+"";
                                case 3: return current.getIP();
                                case 4: {
                                    String req = current.getRequest();
                                    if (req.length() > MAXSTR) {
                                        return req.substring(0, MAXSTR)+"...";
                                    } else {
                                        return req;
                                    }
                                }
                                case 5:
                                    return "[query log]";
                                default: return null;
                                }
                            }

                            public Object getKeyAt(int columnIndex) {
                                return new Integer(current.getID());
                            }
                        };
                }
            };

        Table result = new Table(b, headers);
        result.getColumn(4).setCellRenderer(new
                                            DefaultTableCellRenderer(true));
        result.getColumn(5).setCellRenderer(new
                                            DefaultTableCellRenderer(true));
        result.addTableActionListener(new TableActionAdapter() {
                public void cellSelected(TableActionEvent e) {
                    final ParameterMap params = new ParameterMap();
                    params.setParameter("request_id", e.getRowKey());
                    
                    if (e.getColumn().intValue() == 4) {
                            throw new RedirectSignal(URL.getDispatcherPath() + "/ds/request-info" + params, true);
                    } else if (e.getColumn().intValue() == 5) {
                        throw new RedirectSignal(URL.getDispatcherPath() + "/ds/query-log" + params, true);
                    }
                }
            });
        Label l = new Label("None");
        l.setFontWeight(Label.ITALIC);
        l.setStyleAttr("padding-left: 3em");
        result.setEmptyView(l);
        result.setWidth("100%");
        return result;
    }

    String dashes(int depth) {
        StringBuffer sb = new StringBuffer();
        while (depth-- > 0) sb.append("--");
        return sb.toString();
    }

    class RequestInfoComponent extends com.arsdigita.bebop.SimpleComponent {
        public RequestInfoComponent() {
            super();
        }
        public void generateXML(PageState state, Element parent) {
            Integer request_id = (Integer)state.getValue(m_request_id);
            RequestInfo ri =
                WebDevSupport.getInstance().getRequest(request_id.intValue());
            if (ri != null) {
                Container param_list;
                Container form_list;
                Container comment_list;
                Container headers_list;
                Container stages_list;
                BoxPanel bp = new BoxPanel();
                bp.add(new Label("<h3>Parameters</h3>", false));
                bp.add(param_list = new ColumnPanel(2));
                param_list.add(new Label("Request Start Time:"));
                param_list.add(new Label(ri.getTime()));
                param_list.add(new Label("Request Completion Time: "));
                param_list.add(new Label(ri.getEndTime()));
                param_list.add(new Label("Request Duration: "));
                param_list.add(new Label(ri.getDuration()));
                param_list.add(new Label("IP: "));
                param_list.add(new Label(ri.getIP()));
                param_list.add(new Label("Method: "));
                param_list.add(new Label(ri.getMethod()));
                param_list.add(new Label("URL: "));
                param_list.add(new Label(ri.getURL()));
                param_list.add(new Label("Query: "));
                param_list.add(new Label(StringUtils.quoteHtml( ri.getQuery())));
                param_list.add(new Label("Request Parameters: "));
                param_list.add(form_list = new ColumnPanel(2));
                Iterator iter = ri.getParameterNames();
                while (iter.hasNext()) {
                    String param = (String)iter.next();
                    form_list.add(new Label(param + ":"));
                    form_list.add(new Label(ri.getParameter(param)));
                }

                bp.add(new Label("<h3>Headers</h3>", false));
                bp.add(headers_list = new ColumnPanel(2));
                iter = ri.headerKeys();
                while (iter.hasNext()) {
                    String header = (String)iter.next();
                    headers_list.add(new Label(header +": "));
                    headers_list.add(new Label(ri.getHeader(header)));
                }
                //Doesn't appear to be possible to get the output headers
                //bp.add(new Label("Output Headers"));
                if (ri.numComments() > 0) {
                    bp.add(new Label("<h3>Comments</h3>", false));
                    bp.add(comment_list = new ListPanel(false));
                    iter = ri.getComments();
                    while (iter.hasNext()) {
                        comment_list.add(new Label((String)iter.next()));
                    }
                }
                if (ri.numStages() > 0) {
                    bp.add(new Label("<h3>Stages</h3>", false));
                    bp.add(stages_list = new ColumnPanel(4));
                    iter = ri.getStages();
                    stages_list.add(new Label("Stage"));
                    stages_list.add(new Label("Time"));
                    stages_list.add(new Label("Queries"));
                    stages_list.add(new Label("Processing"));
                    while (iter.hasNext()) {
                        StageInfo si = (StageInfo)iter.next();
                        String leaf = si.leaf() ? "*" : "";
                        int numqueries = si.numQueries();
                        long time = si.time();
                        stages_list.add(new Label(dashes(si.depth()) + si.getName()));
                        stages_list.add(new Label(time + " ms" + leaf));
                        if (numqueries != 0) {
                            long queryTime = si.queryTime(ri.getQueries());
                            long non_queryTime = time-queryTime;
                            stages_list.add(new Label(queryTime + " ms" + leaf + " ("+numqueries+" queries)"));
                            stages_list.add(new Label(non_queryTime + " ms" + leaf));
                        } else {
                            stages_list.add(new Label(""));
                            stages_list.add(new Label(time + " ms"+ leaf));
                        }
                    }
                }
                bp.add(new Label("<h3>Database Requests</h3>", false));
                int query_count = 0;
                long total_time = 0;
                iter = ri.getQueries();
                while (iter.hasNext()) {
                    query_count++;
                    total_time += ((QueryInfo)iter.next()).getTime();
                }
                bp.add(new Label("Total Queries: " + query_count));
                bp.add(new Label("Total Time: " + total_time + " ms"));
                bp.generateXML(state, parent);
            }
        }
    }

    class QueryInfoComponent extends com.arsdigita.bebop.SimpleComponent {
        public QueryInfoComponent() {
            super();
        }
        public void generateXML(PageState state, Element parent) {
            Integer request_id = (Integer)state.getValue(m_query_request_id);
            Integer query_id = (Integer)state.getValue(m_query_id);
            RequestInfo ri =
                WebDevSupport.getInstance().getRequest(request_id.intValue());
            if (ri != null) {
                QueryInfo qi = ri.getQuery(query_id.intValue());
                if (qi != null) {
                    ListPanel info_list;
                    BoxPanel bp = new BoxPanel();
                    bp.add(info_list = new ListPanel(false));
                    info_list.add(new Label("Execution Time: " +
                                            qi.getTime() + " ms"));
                    info_list.add(new Label("Connection ID: " +
                                            qi.getConnectionID()));
                    info_list.add(new Label("Type: " +
                                            qi.getType()));
                    info_list.add(new Label("Query: <br/><pre>" +
                                            StringUtils.quoteHtml( qi.getQuery() ) + "</pre>", false));
                    info_list.add(new Label("Bindvars: " +
                                            qi.getBindvars()));

                    Link l = new Link ("Query Execution Plan",
                                        "../query-plan/");
                    l.setVar("request_id", request_id.toString());
                    l.setVar("query_id", query_id.toString());
                    info_list.add(l);
                    

                    info_list.add(new Label("Exception: " +
                                            qi.getSQLE()));
                    info_list.add(new Label("StackTrace: <br/><pre>" +
                                            StringUtils.quoteHtml(qi.getStackTrace()) + 
                                            "</pre>", false));
                    bp.generateXML(state, parent);
                }
            }
        }
    }

    private ParameterModel m_query_p_id = new IntegerParameter("query_id");
    private ParameterModel m_query_p_request_id =
        new IntegerParameter("request_id");

    private Page buildQueryPlanPage() {
        Page p = PageFactory.buildPage(APP_NAME, "Query Execution Plan");
        p.addGlobalStateParam(m_query_p_request_id);
        p.addGlobalStateParam(m_query_p_id);
        p.add(new QueryPlanComponent());
        p.lock();
        return p;
    }

}

class NonEscapedTableCellRenderer implements TableCellRenderer {
    public NonEscapedTableCellRenderer() {
        super();
    }

    public Component getComponent(Table table, PageState state, Object value,
                                  boolean isSelected, Object key,
                                  int row, int column) {
        SimpleContainer c = new SimpleContainer();
        Label l = new Label((String)value);
        l.setOutputEscaping(false);
        c.add(l);
        c.add(new ControlLink("download"));
        return c;
    }
}
