package de.renew.windowmanagement;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;

import bibliothek.extension.gui.dock.preference.PreferenceModel;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CPreferenceModel;
import bibliothek.gui.dock.common.CWorkingArea;
import bibliothek.gui.dock.common.DefaultMultipleCDockable;
import bibliothek.gui.dock.common.DefaultSingleCDockable;
import bibliothek.gui.dock.common.MultipleCDockable;
import bibliothek.gui.dock.common.MultipleCDockableFactory;
import bibliothek.gui.dock.common.MultipleCDockableLayout;
import bibliothek.gui.dock.common.SingleCDockable;
import bibliothek.gui.dock.common.event.CControlListener;
import bibliothek.gui.dock.common.event.CFocusListener;
import bibliothek.gui.dock.common.event.CKeyboardListener;
import bibliothek.gui.dock.common.event.CVetoClosingListener;
import bibliothek.gui.dock.common.grouping.PlaceholderGrouping;
import bibliothek.gui.dock.common.intern.CDockFrontend;
import bibliothek.gui.dock.common.menu.CLayoutChoiceMenuPiece;
import bibliothek.gui.dock.common.menu.CLookAndFeelMenuPiece;
import bibliothek.gui.dock.common.menu.CPreferenceMenuPiece;
import bibliothek.gui.dock.common.perspective.CDockablePerspective;
import bibliothek.gui.dock.common.perspective.CGridPerspective;
import bibliothek.gui.dock.common.perspective.CPerspective;
import bibliothek.gui.dock.common.theme.ThemeMap;
import bibliothek.gui.dock.facile.lookandfeel.DockableCollector;
import bibliothek.gui.dock.facile.menu.RootMenuPiece;
import bibliothek.gui.dock.support.lookandfeel.ComponentCollector;
import bibliothek.gui.dock.support.lookandfeel.LookAndFeelList;
import bibliothek.gui.dock.support.menu.SeparatingMenuPiece;
import bibliothek.util.Path;

import de.renew.plugin.PluginManager;


/**
 * This class provides the main frame of the Renew application.
 * It controls all contained windows and allows listening to changes to the view parts.
 */
public class WorkbenchImpl implements Workbench {

    private JFrame _mainframe;
    private CControl _controller;
    private RenewMenuBar _fMenu;
    private JTextField _fStatusLine;
    private JPanel _fStaticPanel;

    /**
     * creates a constant String LOWER_LEFT_GROUP where th path for the lower left drawing layout is stored.
     */
    public final static Path LOWER_LEFT_GROUP = new Path("drawinglayout", "lowerleft");
    /**
     * creates a constant String UPPER_LEFT_GROUP where th path for the upper left drawing layout is stored.
     */
    public final static Path UPPER_LEFT_GROUP = new Path("drawinglayout", "upperleft");
    /**
     * creates a constant String UPPER_RIGHT_GROUP where th path for the upper right drawing layout is stored.
     */
    public final static Path UPPER_RIGHT_GROUP = new Path("drawinglayout", "upperright");
    /**
     * creates a constant String LOWER_RIGHT_GROUP where th path for the lower right drawing layout is stored.
     */
    public final static Path LOWER_RIGHT_GROUP = new Path("drawinglayout", "lowerright");

    /**
     * static logger for the WorkbenchImpl.class
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(WorkbenchImpl.class);

    private final static int FMAX_CONSOLE_LINES = 80;

    private CWorkingArea _fDrawingsArea;

    /**
     * creates the main Workbench window and the menubar for it.
     * It also binds the suitable controller fpr the mainframe
     *
     * @param menuBar the MenuBar for the main workbench window.
     * @param frame the JFrame, where you initialize the the workbench window.
     */
    public WorkbenchImpl(RenewMenuBar menuBar, JFrame frame) {

        UIManager.installLookAndFeel("Darcula", "com.bulenkov.darcula.DarculaLaf");

        LOGGER.debug("Starting Workbench.");

        _fMenu = menuBar;

        _fStaticPanel = new JPanel();
        _fStaticPanel.setLayout(new BoxLayout(_fStaticPanel, BoxLayout.PAGE_AXIS));
        _fStaticPanel.setBackground(Color.lightGray);

        _mainframe = frame;
        _mainframe.setTitle("RENEW - the Reference Net Workshop");
        _mainframe.setLayout(new BorderLayout());
        _mainframe.setMinimumSize(new Dimension(800, 600));
        _mainframe.setPreferredSize(new Dimension(1000, 680));
        _controller = new CControl(_mainframe);
        _mainframe.add(_controller.getContentArea());

        // There currently is no proper hotkey management.
        // To not overwrite hotkeys in use by other plugins,
        // for now disable all controller hotkeys.
        _controller.putProperty(CControl.KEY_MAXIMIZE_CHANGE, null);
        _controller.putProperty(CControl.KEY_GOTO_MAXIMIZED, null);
        _controller.putProperty(CControl.KEY_GOTO_MINIMIZED, null);
        _controller.putProperty(CControl.KEY_CANCEL_OPERATION, null);
        _controller.putProperty(CControl.KEY_CLOSE, null);
        _controller.putProperty(CControl.KEY_GOTO_EXTERNALIZED, null);
        _controller.putProperty(CControl.KEY_GOTO_NORMALIZED, null);

        _mainframe.getContentPane().add(_fStaticPanel, "North");
        _mainframe.setJMenuBar(_fMenu);

        // This is an old workaround to dispatch key events from externalized windows to the menu
        new HotDrawFocusManager(_fMenu);

        PreferenceModel preferences = new CPreferenceModel(_controller);
        _controller.setPreferenceModel(preferences);
        RootMenuPiece system = new RootMenuPiece("System", false);
        system.add(
            new SeparatingMenuPiece(new CLookAndFeelMenuPiece(_controller), false, false, true));

        RootMenuPiece layout = new RootMenuPiece("Layout", false);
        layout.add(new CLayoutChoiceMenuPiece(_controller, true));
        system.add(new SeparatingMenuPiece(layout, false, false, true));
        system.add(new CPreferenceMenuPiece(_controller));


        try {
            JMenu systemMenu = system.getMenu();
            systemMenu.addSeparator();

            for (JMenuItem item : UIScaleMenuExtender.createMenus()) {
                systemMenu.add(item);
            }
            _fMenu.addMenu(systemMenu, RenewMenuBar.SYSTEMS_SECTION);
        } catch (UnknownMenuSectionException e) {
            e.printStackTrace();
        }

        _mainframe.getContentPane().add(_controller.getContentArea(), BorderLayout.CENTER);
        _fDrawingsArea = _controller.createWorkingArea("drawing area");
        initializeControl(_controller);

        _fStatusLine = new JTextField(40);
        _fStatusLine.setEditable(false);
        _mainframe.add(_fStatusLine, BorderLayout.SOUTH);
        showStatus("Welcome to Renew - the Reference Net Workshop");

        // Maximize screen size if possible
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        _mainframe.setSize(screenSize.width, screenSize.height);
        _mainframe.pack();
        _mainframe.setVisible(true);

        // Update internal components after look&feel changes
        ComponentCollector collector = new DockableCollector(_controller.intern());
        LookAndFeelList.getDefaultList().addComponentCollector(collector);

        // Try to load settings from disk
        CDockFrontend frontend = _controller.intern();
        File file = new File(PluginManager.getPreferencesLocation(), "renewLayoutSettings");
        if (file.exists()) {
            try {
                FileInputStream fileInputStream = new FileInputStream(file);
                DataInputStream dataInputStream = new DataInputStream(fileInputStream);
                frontend.read(dataInputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                _controller.setTheme(ThemeMap.KEY_FLAT_THEME);
            }
        });

    }

    /**
     * Provides the {@link CControl} with its initial perspective.
     * <p>
     * ____  __________  ____
     * | ul ||          || ur |
     * |____||  drawing ||____|
     * |    ||          ||    |
     * | low||   area   || low|
     * | <- ||          || -> |
     * |____||__________||____|
     *
     * @param control is the {@Link CControl} with its initial perspective.
     */
    private void initializeControl(CControl control) {

        LOGGER.debug("Initializing base perspective.");

        CPerspective perspective = control.getPerspectives().createEmptyPerspective();
        CGridPerspective center = perspective.getContentArea().getCenter();

        center.gridPlaceholder(0, 0, 20, 10, UPPER_LEFT_GROUP);
        center.gridPlaceholder(0, 10, 20, 20, LOWER_LEFT_GROUP);
        center
            .gridAdd(20, 0, 50, 30, (CDockablePerspective) perspective.getStation("drawing area"));
        center.gridPlaceholder(70, 0, 20, 10, UPPER_RIGHT_GROUP);
        center.gridPlaceholder(70, 10, 20, 20, LOWER_RIGHT_GROUP);

        control.getPerspectives().setPerspective("basePerspective", perspective, true);
        control.load("basePerspective");
    }

    @Override
    public JFrame getMainFrame() {

        return _mainframe;
    }

    @Override
    public void showStatus(final String string) {

        Runnable r = new Runnable() {
            @Override
            public void run() {
                _fStatusLine.setText(string);
            }
        };

        if (EventQueue.isDispatchThread()) {
            r.run();
        } else {
            EventQueue.invokeLater(r);
        }
    }

    @Override
    public void addStaticPanel(JPanel panel) {
        _fStaticPanel.add(panel);
        _mainframe.pack();
    }

    @Override
    public void addEditorWindow(final MultipleCDockableLayout layout, final String factoryID) {

        LOGGER.debug("Adding new Editor window for layout " + layout.toString());
        LOGGER.debug("Using factoryID " + factoryID);

        Runnable r = new Runnable() {
            @Override
            public void run() {

                MultipleCDockableFactory factory =
                    _controller.getMultipleDockableFactory(factoryID);
                MultipleCDockable editor = factory.read(layout);
                _controller.addDockable(editor);
                editor.setWorkingArea(_fDrawingsArea);

                if (editor instanceof DefaultMultipleCDockable) {
                    ((DefaultMultipleCDockable) editor).setTitleShown(true);
                }

                editor.setVisible(true);
            }
        };

        if (EventQueue.isDispatchThread()) {

            r.run();

        } else {

            EventQueue.invokeLater(r);
        }

    }

    @Override
    public void newEditorWindow(String factoryID) {

        LOGGER.debug("Attempting to create new empty editor template for factoryID " + factoryID);
        MultipleCDockableFactory factory = _controller.getMultipleDockableFactory(factoryID);
        addEditorWindow(factory.create(), factoryID);
    }


    @Override
    public void registerControlListener(CControlListener listener) {

        LOGGER.debug("Add control listener " + listener.toString());
        _controller.addControlListener(listener);
    }


    @Override
    public synchronized void registerEditorFactory(MultipleCDockableFactory factory, String id) {

        LOGGER.debug("Registering new editor factory under id " + id);

        if (_controller.getMultipleDockableFactory(id) != null) {
            return;
        }
        _controller.addMultipleDockableFactory(id, factory);

    }


    @Override
    public void registerMenu(JMenu menu) {
        _fMenu.add(menu);
        updateFrameSize();
    }


    @Override
    public void registerMenu(JMenu menu, int index) throws UnknownMenuSectionException {
        _fMenu.addMenu(menu, index);
        updateFrameSize();
    }


    @Override
    public void removeMenu(JMenu menu) {
        _fMenu.remove(menu);
        updateFrameSize();
    }

    /**
     * Update the mainframe size after menu changes.
     *
     * Sets the preferred size to the current size before calling {@code pack()} to prevent
     * the frame from resizing to default dimensions when menus are added or removed.
     * This fixes the issue where opening/closing drawings caused unwanted frame resizing.
     */
    private void updateFrameSize() {
        Dimension current = _mainframe.getSize();
        _mainframe.setPreferredSize(current);
        _mainframe.pack();
    }


    @Override
    public void close(MultipleCDockable dockable) {
        dockable.setVisible(false);
        if (!dockable.isVisible()) {
            // If container is still visible, operation was canceled by user.
            _controller.removeDockable(dockable);
        }
    }

    @Override
    public void close(SingleCDockable dockable) {
        dockable.setVisible(false);
        if (!dockable.isVisible()) {
            // If container is still visible, operation was canceled by user.
            _controller.removeDockable(dockable);
        }
    }


    @Override
    public void addViewWindow(final DefaultSingleCDockable dockable, final Path path) {

        LOGGER.debug("Add new view " + dockable.getTitleText());
        LOGGER.debug("At position " + path.toString());

        Runnable r = new Runnable() {
            @Override
            public void run() {
                dockable.setGrouping(new PlaceholderGrouping(_controller, path));
                _controller.addDockable(dockable);
                dockable.setVisible(true);
            }
        };

        if (EventQueue.isDispatchThread()) {
            r.run();
        } else {
            try {
                EventQueue.invokeAndWait(r);
            } catch (InterruptedException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

    }

    /**
     * Destroy the mainframe and controller.
     */
    protected void destroy() {

        LOGGER.debug("Saving current layout to disk.");

        CDockFrontend frontend = _controller.intern();
        File file = new File(PluginManager.getPreferencesLocation(), "renewLayoutSettings");
        // Create new file if it doesn't exist
        try {
            file.createNewFile();

            FileOutputStream fileOutputStream = new FileOutputStream(file);
            DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
            frontend.write(dataOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }

        _mainframe.setVisible(false);

        // There might be unfinished operations requiring access to the mainframe.
        // Since all of those and this method run on the awt EventQueue thread, we can ensure
        // they are finished and the proxy is freed before finally destroying the mainframe.
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                LOGGER.debug("Destroying controller and mainframe.");

                _controller.destroy();
                _mainframe.dispose();
            }
        });

    }

    @Override
    public void registerShutdownHook(RenewShutdownHook hook) {
        // This is done in the proxy, so no need for this method here.
    }

    @Override
    public void openGui() {
        _mainframe.setVisible(true);
    }

    @Override
    public void registerFocusListener(CFocusListener listener) {
        _controller.addFocusListener(listener);
    }

    @Override
    public void registerVetoClosingListener(CVetoClosingListener listener) {
        _controller.addVetoClosingListener(listener);
    }

    @Override
    public void registerKeyboardListener(CKeyboardListener listener) {
        _controller.addKeyboardListener(listener);
    }
}
