/*
 *  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 openbook.client;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;

import org.apache.openjpa.conf.OpenJPAVersion;
import org.apache.openjpa.lib.jdbc.JDBCListener;
import org.apache.openjpa.persistence.OpenJPAPersistence;

import jpa.tools.swing.AttributeLegendView;
import jpa.tools.swing.ConfigurationViewer;
import jpa.tools.swing.ErrorDialog;
import jpa.tools.swing.GraphicOutputStream;
import jpa.tools.swing.MetamodelView;
import jpa.tools.swing.PowerPointViewer;
import jpa.tools.swing.PreparedQueryViewer;
import jpa.tools.swing.ScrollingTextPane;
import jpa.tools.swing.StatusBar;
import jpa.tools.swing.SwingHelper;
import openbook.domain.Customer;
import openbook.server.OpenBookService;
import openbook.server.ServiceFactory;
import openbook.util.PropertyHelper;

/**
 * A graphical user interface based client of OpenBooks for demonstration.
 *
 * @author Pinaki Poddar
 *
 */
public class Demo extends JFrame {
    private static final long serialVersionUID = 1L;
    private static Dimension TAB_VIEW = new Dimension(800,600);
    private static Dimension OUT_VIEW = new Dimension(800,200);
    private static Dimension NAV_VIEW = new Dimension(400,800);

    /**
     * The actions invoked by this sample demonstration.
    */
    private Action _root;
    private Action _about;
    private Action _buyBook;
    private Action _deliver;
    private Action _supply;
    private Action _viewConfig;
    private Action _viewDomain;
    private Action _viewSource;
    private Action _viewQuery;

    /**
     * The primary graphic widgets used to invoke and display the results of the actions.
     */
    private JToolBar    _toolBar;
    private JTree       _navigator;
    private JTabbedPane _tabbedPane;
    private JTabbedPane _outputPane;
    private StatusBar   _statusBar;
    private ScrollingTextPane   _sqlLog;
    private SQLLogger _sqlListener;
    private SourceCodeBrowser _sourceBrowser;
    private static Demo _instance;
    private static final String SRC_ROOT = "source/";

    /**
     * The handle to the service.
     */
    private OpenBookService     _service;
    private Customer            _customer;
    private Map<String, Object> _config;

    /**
     * Runs the demo.
     */
    public static void main(String[] args) throws Exception {
        SwingHelper.setLookAndFeel(14);
        adjustWidgetSize();
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                Demo demo = Demo.getInstance();
                demo.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

                demo.pack();
                SwingHelper.position(demo, null);
                demo.setVisible(true);
            }
        });
    }

    public synchronized static Demo getInstance() {
        if (_instance == null) {
            _instance = new Demo();
        }
        return _instance;
    }

    static void adjustWidgetSize() {
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        int sw = (int)(95*screen.getWidth()/100);
        int sh = (int)(80*screen.getHeight()/100);
        NAV_VIEW = new Dimension(25*sw/100, sh);
        TAB_VIEW = new Dimension(75*sw/100, 65*sh/100);
        OUT_VIEW = new Dimension(75*sw/100, 35*sh/100);
    }


    private Demo() {
        Thread.currentThread().setUncaughtExceptionHandler(new ErrorHandler());
        _config = PropertyHelper.load(System.getProperty("openbook.client.config", "demo.properties"));
        setIconImage(((ImageIcon)Images.LOGO_OPENBOOKS).getImage());
        setTitle("OpenBooks: A Sample JPA 2.0 Application");

        _root         = new WelcomeAction("OpenBooks", Images.LOGO_OPENBOOKS, "OpenBooks");
        _about        = new AboutAction("About OpenBooks", Images.LOGO_OPENBOOKS, "About OpenBooks");
        _buyBook      = new BuyBookAction("Buy", Images.BUY, "Browse and Buy Books");
        _deliver      = new DeliveryAction("Deliver", Images.DELIVERY, "Deliver Pending Orders");
        _supply       = new SupplyAction("Supply", Images.SUPPLY, "Supply Books");
        _viewConfig   = new ViewConfigAction("Unit", Images.VIEW_UNIT, "View Configuration");
        _viewDomain   = new ViewDomainAction("Domain", Images.VIEW_DOMAIN, "View Domain Model");
        _viewSource   = new ViewSourceAction("Source", Images.VIEW_CODE, "View Source Code");
        _viewQuery    = new ViewQueryCacheAction("Query", Images.VIEW_QUERY, "View Queries");

        _toolBar    = createToolBar();
        _navigator  = createNavigator();
        _tabbedPane = createTabbedView();
        _outputPane = createOutputView();
        _statusBar  = createStatusBar();

        JSplitPane horizontalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        horizontalSplitPane.setContinuousLayout(true);
        horizontalSplitPane.setDividerSize(5);
        JScrollPane scrollPane = new JScrollPane(_navigator);
        scrollPane.setMinimumSize(new Dimension(NAV_VIEW.width/4, NAV_VIEW.height));
        scrollPane.setPreferredSize(NAV_VIEW);
        horizontalSplitPane.add(scrollPane);

        JSplitPane verticalSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        verticalSplitPane.setContinuousLayout(true);
        verticalSplitPane.setDividerSize(5);
        verticalSplitPane.add(_tabbedPane);
        verticalSplitPane.add(_outputPane);
        horizontalSplitPane.add(verticalSplitPane);

        Container content = getContentPane();
        content.add(_toolBar, BorderLayout.PAGE_START);
        content.add(horizontalSplitPane, BorderLayout.CENTER);
        content.add(_statusBar, BorderLayout.SOUTH);

        _root.actionPerformed(null);
    }

    /**
     * Gets the handle to OpenBooks service.
     */
    public OpenBookService getService() {
        if (_service == null) {
            final String unitName = getConfiguration("openbook.unit", OpenBookService.DEFAULT_UNIT_NAME);

            SwingWorker<OpenBookService, Void> getService = new SwingWorker<OpenBookService, Void> () {
                @Override
                protected OpenBookService doInBackground() throws Exception {
                    Map<String, Object> runtimeConfig = new HashMap<>();
                    runtimeConfig.put("openjpa.jdbc.JDBCListeners", new JDBCListener[]{_sqlListener});
                    OpenBookService service = ServiceFactory.getService(unitName, runtimeConfig);
                    service.initialize(null);
                    return service;
                }

            };
            getService.execute();
            try {
                setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
                _service = getService.get(10, TimeUnit.SECONDS);
            } catch (Exception t) {
                new ErrorDialog(t).setVisible(true);
            } finally {
                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
            }
        }
        return _service;
    }

    public Customer getCustomer() {
        if (_customer == null) {
            SwingWorker<Customer, Void> task = new SwingWorker<Customer, Void> () {
                @Override
                protected Customer doInBackground() throws Exception {
                    return getService().login("guest");
                }

            };
            task.execute();
            try {
                _customer = task.get(1, TimeUnit.SECONDS);
            } catch (Exception t) {
                new ErrorDialog(t).setVisible(true);
            }
        }
        return _customer;
    }

    private JToolBar  createToolBar() {
        JToolBar toolBar = new JToolBar();
        toolBar.add(_buyBook);
        toolBar.add(_deliver);
        toolBar.add(_supply);
        Dimension d = new Dimension(40, 32);
        toolBar.addSeparator(d);

        toolBar.add(_viewConfig);
        toolBar.add(_viewDomain);
        toolBar.add(_viewSource);
        toolBar.add(_viewQuery);

        toolBar.addSeparator(d);

        toolBar.add(Box.createHorizontalGlue());
        toolBar.add(_about);
        toolBar.add(Box.createHorizontalStrut(2));
        return toolBar;
    }

    private StatusBar createStatusBar() {
        return new StatusBar();
    }

    public String getConfiguration(String key, String def) {
        return PropertyHelper.getString(_config, key, def);
    }

    private SourceCodeBrowser getSourceCodeBrowser() {
        if (_sourceBrowser == null) {
            String root = getConfiguration("openbook.source.root",  SRC_ROOT);
            boolean external = "true".equalsIgnoreCase(
                    getConfiguration("openbook.source.browser.external", "false"))
                    && Desktop.isDesktopSupported();
            _sourceBrowser = new SourceCodeBrowser(root, false);
            if (!external) {
                Map<String,String> initialPages = PropertyHelper.getMap(_config, "openbook.source.list");
                for (Map.Entry<String, String> entry : initialPages.entrySet()) {
                    _sourceBrowser.addPage(entry.getKey(), entry.getValue());
                }
                showTab(_tabbedPane, "Source Code", _sourceBrowser.getViewer());
            }
        }
        return _sourceBrowser;
    }

    /**
     * Abstract root of all Action objects helps to locate/configure visual action parameters such as
     * tooltip text or image.
     *
     * @author Pinaki Poddar
     *
     */
    public abstract class OpenBookAction extends AbstractAction {
        
        private static final long serialVersionUID = 1L;

        public OpenBookAction(String name, Icon icon, String tooltip) {
            putValue(Action.NAME, name);
            putValue(Action.SHORT_DESCRIPTION, tooltip);
            putValue(Action.SMALL_ICON, icon);
        }
    }

    public class BuyBookAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        BuyBookPage         _buyBookPage;
        public BuyBookAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (_buyBookPage == null) {
                _buyBookPage = new BuyBookPage(getService(), getCustomer());
            }
            showTab(_tabbedPane, "Buy Books", _buyBookPage);
            switchTab(_outputPane, _sqlLog);
        }

    }
    public class DeliveryAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        DeliveryPage        _deliveryPage;
        public DeliveryAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            if (_deliveryPage == null) {
                _deliveryPage = new DeliveryPage(getService());
            }
            showTab(_tabbedPane, "Deliver Books", _deliveryPage);
            switchTab(_outputPane, _sqlLog);
        }

    }

    public class SupplyAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        SupplyPage          _supplyPage;
        public SupplyAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            if (_supplyPage == null) {
                _supplyPage = new SupplyPage(getService());
            }
            showTab(_tabbedPane, "Supply Books", _supplyPage);
            switchTab(_outputPane, _sqlLog);
        }

    }

    public class ViewConfigAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        ConfigurationViewer _configView;
        public ViewConfigAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            if (_configView == null) {
                _configView = new ConfigurationViewer("Unit Configuration", getService().getUnit().getProperties());
                showTab(_tabbedPane, "Configuration", new JScrollPane(_configView));
            } else {
                showTab(_tabbedPane, "Configuration", _configView);
            }
        }

    }

    public class ViewDomainAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        MetamodelView       _domainView;
        AttributeLegendView _legends;
        public ViewDomainAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            if (_domainView == null) {
                _domainView = new MetamodelView(getService().getUnit().getMetamodel());
                _legends = new AttributeLegendView();
                showTab(_outputPane, "Legends", new JScrollPane(_legends));
            }
            showTab(_tabbedPane, "Domain Model", _domainView);
        }

    }

    public class ViewDataAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        public ViewDataAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            showTab(_tabbedPane, "Buy Books", null);
        }

    }

    public class ViewQueryCacheAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        PreparedQueryViewer _queryView;
        public ViewQueryCacheAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }
        @Override
        public void actionPerformed(ActionEvent e) {
            if (_queryView == null) {
                _queryView = new PreparedQueryViewer(OpenJPAPersistence.cast(getService().getUnit()));
                showTab(_tabbedPane, "JPQL Query", new JScrollPane(_queryView));
            }
            showTab(_tabbedPane, "JPQL Queries", _queryView);
        }

    }

    public class ViewSourceAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;

        public ViewSourceAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            getSourceCodeBrowser();
        }
    }

    /**
     * An action to show a piece of code in an internal or external browser.
     *
     */
    public class ShowCodeAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        private String _key;
        private String _page;

        public ShowCodeAction() {
            super("View Code", Images.VIEW_CODE, "View Java Source Code");
        }

        public void setPage(String key, String page) {
            _key  = key;
            _page = page;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
           getSourceCodeBrowser().showPage(_key, _page);
        }
    }

    /**
     * Displays the "welcome" page.
     *
     * @author Pinaki Poddar
     *
     */
    public class WelcomeAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        PowerPointViewer    _powerpoint;
        JLabel              _logoLabel = new JLabel(Images.LOGO_OPENBOOKS);
        boolean _showPresentation = true;

        public WelcomeAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (_powerpoint == null && _showPresentation) {
                String dir = getConfiguration("openbook.slides.dir", "slides/");
                String[] defaultSlides = {
                                    "Slide1.JPG",
                                    "Slide2.JPG",
                                    "Slide3.JPG",
                                    "Slide4.JPG",
                                    "Slide5.JPG",
                                    "Slide6.JPG",
                                    "Slide7.JPG",
                                    "Slide8.JPG",
                                    "Slide9.JPG",
                                    "Slide10.JPG",
                                    "Slide11.JPG",
                                    "Slide12.JPG",
                                    "Slide13.JPG",
                                    "Slide14.JPG",
                                    "Slide15.JPG"};
                List<String> slides = PropertyHelper.getStringList(_config, "openbook.slides.list",
                        Arrays.asList(defaultSlides));
                try {
                    _powerpoint = new PowerPointViewer(dir, slides);
                } catch (Exception e1) {
                    _showPresentation = false;
                    System.err.println("Error while opening slide deck at " + dir + ". \r\n"+ e1);
                }
            }
            showTab(_tabbedPane, "Home", _powerpoint != null ? _powerpoint : _logoLabel);
        }

    }

    public class AboutAction extends OpenBookAction {
        
        private static final long serialVersionUID = 1L;
        AboutDialog _dialog;

        public AboutAction(String name, Icon icon, String tooltip) {
            super(name, icon, tooltip);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (_dialog == null) {
                _dialog = new AboutDialog(Images.LOGO_OPENBOOKS);
                SwingHelper.position(_dialog, Demo.this);
            }
            _dialog.setVisible(true);
        }
    }

    /**
     * Show the given tab in the given pane.
     * @param pane the tabbed pane
     * @param title title of the tab component
     * @param tab the component to show
     */
    void showTab(JTabbedPane pane, String title, Component tab) {
        if (tab == null)
            return;
        Component c = locateTab(pane, tab);
        if (c == null) {
            pane.addTab(title, tab);
            pane.setSelectedComponent(tab);
        } else {
            pane.setSelectedComponent(c);
        }
    }

    void switchTab(JTabbedPane pane, Component tab) {
        if (tab == null)
            return;
        Component c = locateTab(pane, tab);
        if (c == null) {
            pane.setSelectedComponent(c);
        }
    }

    Component locateTab(JTabbedPane pane, Component tab) {
        int index = pane.indexOfComponent(tab);
        if (index == -1) {
            Component[] components = pane.getComponents();
            for (int i = 0; i < components.length; i++) {
                if (components[i] instanceof JScrollPane
                && (((JScrollPane)components[i]).getViewport().getView() == tab)) {
                    return components[i];
                }
            }
        } else {
            return pane.getComponentAt(index);
        }
        return null;
    }


    private JTabbedPane createTabbedView() {
        JTabbedPane pane = new JTabbedPane();
        pane.setPreferredSize(TAB_VIEW);
        pane.setMinimumSize(new Dimension(TAB_VIEW.width, TAB_VIEW.height));
        return pane;
    }

    private JTabbedPane createOutputView() {
        JTabbedPane pane = new JTabbedPane();
        pane.setPreferredSize(OUT_VIEW);

        _sqlListener = new SQLLogger();
        _sqlLog = new ScrollingTextPane();

        GraphicOutputStream stream = new GraphicOutputStream(_sqlLog);
        _sqlLog.setPreferredSize(TAB_VIEW);
        _sqlListener.setOutput(stream);
        pane.addTab("SQL Log", new JScrollPane(_sqlLog));

        ScrollingTextPane consoleLog = new ScrollingTextPane();
        GraphicOutputStream console = new GraphicOutputStream(consoleLog);
        System.setErr(new PrintStream(console, true));
        pane.addTab("Console", new JScrollPane(consoleLog));

        return pane;
    }

    /**
     * Creates the navigation tree and adds the tree nodes. Each tree node is attached with an action
     * that fires when the node is selected.
     */
    private JTree createNavigator() {
        ActionTreeNode root = new ActionTreeNode(_root);
        DefaultMutableTreeNode app   = new DefaultMutableTreeNode("WorkFlows");
        DefaultMutableTreeNode views = new DefaultMutableTreeNode("Views");
        root.add(app);
        root.add(views);


        app.add(new ActionTreeNode(_buyBook));
        app.add(new ActionTreeNode(_deliver));
        app.add(new ActionTreeNode(_supply));

        views.add(new ActionTreeNode(_viewConfig));
        views.add(new ActionTreeNode(_viewDomain));
        views.add(new ActionTreeNode(_viewQuery));
        views.add(new ActionTreeNode(_viewSource));


        JTree tree = new JTree(root);
        tree.setShowsRootHandles(true);

        tree.addTreeSelectionListener(new TreeSelectionListener() {
            @Override
            public void valueChanged(TreeSelectionEvent e) {
                Object treeNode = _navigator.getLastSelectedPathComponent();
                if (treeNode instanceof ActionTreeNode) {
                    ((ActionTreeNode)treeNode)._action.actionPerformed(null);
                }
            }
        });
        tree.setCellRenderer(new TypedTreeCellRenderer());

        for (int i = 0; i < tree.getRowCount(); i++) {
            tree.expandRow(i);
        }

        return tree;
    }

    /**
     * A tree node which may have an associated action.
     *
     * @author Pinaki Poddar
     *
     */
    public static class ActionTreeNode extends DefaultMutableTreeNode {
        
        private static final long serialVersionUID = 1L;
        private final Action _action;
        public ActionTreeNode(Action action) {
            _action = action;
        }

        @Override
        public String toString() {
            return _action.getValue(Action.SHORT_DESCRIPTION).toString();
        }

    }

    public class TypedTreeCellRenderer extends DefaultTreeCellRenderer {
        
        private static final long serialVersionUID = 1L;

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
                boolean leaf, int row, boolean hasFocus) {
            return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
        }
    }

    public static class AboutDialog extends JDialog {
        
        private static final long serialVersionUID = 1L;

        public AboutDialog(Icon logo) {
            setModal(true);
            setLayout(new BorderLayout());
            JButton button = new JButton("<html>"
                    + "<b>OpenBooks</b> "
                    + "<p>"
                    + "<br> by JPA Team, SWG"
                    + "<br> IBM Corporation"
                    + "<p>"
                    + "</html>");
            button.setIcon(logo);
            button.setHorizontalTextPosition(SwingConstants.RIGHT);
            button.setIconTextGap(4);
            button.setEnabled(true);
            button.setBorderPainted(false);
            JLabel openJPALogo = new JLabel(Images.LOGO_OPENJPA);
            String version = "Version " + OpenJPAVersion.MAJOR_RELEASE + "." + OpenJPAVersion.MINOR_RELEASE;
            openJPALogo.setBorder(BorderFactory.createTitledBorder(version));
            openJPALogo.setBackground(Color.BLACK);

            add(button, BorderLayout.CENTER);
            add(openJPALogo, BorderLayout.SOUTH);

            setTitle("About OpenBooks");
            setAlwaysOnTop(true);
            setResizable(false);
            pack();
        }
    }

}
