/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.components.cpacs.vampzeroinitializer.gui;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.forms.widgets.TableWrapData;
import org.eclipse.ui.forms.widgets.TableWrapLayout;

import de.rcenvironment.components.cpacs.vampzeroinitializer.gui.model.Component;
import de.rcenvironment.components.cpacs.vampzeroinitializer.gui.model.Discipline;
import de.rcenvironment.components.cpacs.vampzeroinitializer.gui.model.Parameter;
import de.rcenvironment.core.gui.utils.common.components.PropertyTabGuiHelper;
import de.rcenvironment.core.utils.common.xml.XMLException;

/**
 * GUI-Controller handling the main Vampzero application. This controller doesn't control resource handles, it's all delegated to the
 * factory and the caller.
 * 
 * @author Arne Bachmann
 * @author Markus Kunde
 */
public class MainGuiController {

    private static final int SIZE_200 = 200;

    /**
     * The logger.
     */
    private static final Log LOGGER = LogFactory.getLog(MainGuiController.class);

    /**
     * Contains all parameters that were modified by the user.
     */
    private List<Component> modifiedComponents = new ArrayList<Component>();

    /**
     * Gui creation factory.
     */
    private FormToolkitSwtHelper factory;

    /**
     * The tree viewer.
     */
    private TreeViewer viewer;

    /**
     * Listener to call when "store cpacs" is clicked.
     */
    private InputTransferable listener;

    /**
     * Path to a gui.xml on the local file system.
     */
    private String xmlPath = null;

    /**
     * All widgets to dispose after closing the app.
     */
    private List<Control> disposables = new LinkedList<Control>();

    /**
     * All components in the GUI definition file.
     */
    private List<Component> components;
    
    private GuiInputParser guiInputParser = new GuiInputParser();
    
    private ToolspecificOutputWriter toolspecificOutputWriter = new ToolspecificOutputWriter();

    /**
     * Constructor.
     * 
     * @param factory The factory to use when creating this gui.
     * @param performListener The listener to activate when hitting save
     */
    public MainGuiController(final FormToolkitSwtHelper factory, final InputTransferable performListener) {
        this.factory = factory;
        this.listener = performListener;
    }

    /**
     * Constructor.
     * 
     * @param factory The factory to use when creating this gui.
     * @param performListener The listener to activate when hitting save
     * @param guiXmlpath Local file system path to load the GUI description from
     */
    public MainGuiController(final FormToolkitSwtHelper factory, final InputTransferable performListener, final String guiXmlPath) {
        this.factory = factory;
        this.listener = performListener;
        this.xmlPath = guiXmlPath;
    }

    /**
     * Remove all unmanaged widgets, in Eclipse most are managed by the FormFactory.
     */
    public void dispose() {
        while (disposables.size() > 0) {
            if (disposables instanceof LinkedList) {
                ((LinkedList<Control>) disposables).getLast().dispose();
                ((LinkedList<Control>) disposables).removeLast();
            }
        }
    }

    /**
     * Gui creation helper. Use either the provided file path or load a standard GUI config from the bundle.
     * 
     * @return The main controls
     */
    public Composite createControls() {
        List<Component> componentsTemp = null;
        if ((xmlPath != null) && (new File(xmlPath).canRead())) {
            FileInputStream fis;
            boolean killSwitch = false;
            try {
                fis = new FileInputStream(xmlPath);
                componentsTemp = guiInputParser.parse(fis);
                fis.close();
            } catch (XPathExpressionException | IOException | XMLException e) {
                LOGGER.error(e);
                killSwitch = true;
            }
            if (killSwitch) {
                return null;
            }
        } else {
            try {
                componentsTemp = guiInputParser.parse(getClass().getClassLoader().getResourceAsStream("resources/gui.xml"));
            } catch (final XPathExpressionException | XMLException e) {
                LOGGER.error(e.getCause().getMessage());
                componentsTemp = new ArrayList<Component>();
            }
        }
        components = componentsTemp;

        final Composite mainComposite = factory.createMainComposite();
        disposables.add(mainComposite);
        final TableWrapLayout layout = new TableWrapLayout();
        layout.numColumns = 5;
        layout.horizontalSpacing = 4;
        layout.verticalSpacing = 4;
        mainComposite.setLayout(layout);

        // drop down boxes and main mask
        final Composite comboComposite = factory.createComposite(mainComposite, 3, 3);
        disposables.add(comboComposite);

        final Combo componentCombo = factory.createCombo(comboComposite, null, null);
        final Combo disciplineCombo = factory.createCombo(comboComposite, null, null);
        final Button loadButton = factory.createButton(comboComposite, "Load configuration...", new Listener() {

            @Override
            public void handleEvent(final Event event) {
                IFile file = PropertyTabGuiHelper.selectFileFromProjects(comboComposite.getShell(), "Foo", "FooBar");
                if (file != null) {
                    StringWriter writer = new StringWriter();
                    try {
                        IOUtils.copy(file.getContents(), writer);
                        String theString = writer.toString();
                        setSelectedParameters(theString);
                    } catch (IOException e) {
                        LOGGER.error("Cannot read content from file.");
                    } catch (CoreException e) {
                        LOGGER.error("Cannot read content from file.");
                    }
                }
            }
        });
        disposables.add(componentCombo);
        disposables.add(disciplineCombo);
        disposables.add(loadButton);

        final Label separator = factory.createSeparator(mainComposite, true);
        ((TableWrapData) separator.getLayoutData()).valign = TableWrapData.FILL;
        ((TableWrapData) separator.getLayoutData()).rowspan = 2;
        disposables.add(separator);

        final Composite rightComposite = factory.createComposite(mainComposite);
        disposables.add(rightComposite);
        final TableWrapData td = new TableWrapData();
        td.align = TableWrapData.FILL;
        td.valign = TableWrapData.FILL;
        td.grabHorizontal = true;
        td.grabVertical = true;
        td.rowspan = 2;
        rightComposite.setLayoutData(td);
        rightComposite.setLayout(new GridLayout(1, false));
        final Button createButton = new Button(rightComposite, SWT.PUSH | SWT.FLAT);
        disposables.add(createButton);
        createButton.setText("Create CPACS");
        createButton.addListener(SWT.Selection, new Listener() {

            @Override
            public void handleEvent(final Event event) {
                listener.transfer(toolspecificOutputWriter.createOutput(modifiedComponents));
            }
        });
        createButton.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
        createButton.setImage(factory.saveImage);
        viewer = new TreeViewer(rightComposite, SWT.SINGLE | SWT.READ_ONLY | SWT.BORDER);

        disposables.add(viewer.getTree());
        viewer.setContentProvider(new ParameterTreeContentProvider());
        viewer.setLabelProvider(new ParameterTreeLabelProvider(viewer.getTree().getDisplay()));
        ColumnViewerToolTipSupport.enableFor(viewer);
        final Set<List<Component>> set = new HashSet<List<Component>>();
        set.add(modifiedComponents); // can't use newSet(collection...) because then not the list
                                     // but its contents are added,which are none
        viewer.setInput(set); // create root "set" with the list of all components in it
        final GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        gd.minimumHeight = SIZE_200;
        gd.minimumWidth = SIZE_200;

        final Composite contentComposite = factory.createComposite(mainComposite, 1, 1);
        disposables.add(contentComposite);

        contentComposite.setLayoutData(new TableWrapData(TableWrapData.FILL, TableWrapData.FILL, 1, 1));
        contentComposite.setLayout(new GridLayout(1, false));

        // initiall combo fillings
        for (final Component component : components) {
            componentCombo.add(component.getName());
        }
        for (final Discipline discipline : components.get(0).getDisciplines()) {
            disciplineCombo.add(discipline.getName());
        }

        final ParameterCompositeWrapper parameterCompositeWrapper =
            new ParameterCompositeWrapper(factory, contentComposite, modifiedComponents);
        parameterCompositeWrapper.setDiscipline(components.get(0).getDisciplines().get(0));
        parameterCompositeWrapper.setViewer(viewer);
        viewer.getTree().setLayoutData(gd);
        viewer.getTree().addListener(SWT.Selection,
            getTreeViewListener(componentCombo, disciplineCombo, parameterCompositeWrapper, contentComposite));
        viewer.getTree().addKeyListener(getKeyAdapter());
        componentCombo.addListener(SWT.Selection, new Listener() {

            @Override
            public void handleEvent(final Event event) {
                final String newSelection = componentCombo.getText();
                final Component component = getComponentForName(components, newSelection);
                fillDisciplines(disciplineCombo, component);
                parameterCompositeWrapper.setDiscipline(component.getDisciplines().get(0));
                parameterCompositeWrapper.redraw();
                disciplineCombo.select(0);
            }
        });
        disciplineCombo.addListener(SWT.Selection, getDisciplineComboListener(disciplineCombo, parameterCompositeWrapper, componentCombo));
        componentCombo.select(0); // initial selection is not empty
        disciplineCombo.select(0);
        return mainComposite;
    }

    private Listener getDisciplineComboListener(final Combo disciplineCombo, final ParameterCompositeWrapper parameterCompositeWrapper,
        final Combo componentCombo) {
        return new Listener() {

            @Override
            public void handleEvent(final Event event) {
                final String newSelection = disciplineCombo.getText();
                final Component component = getComponentForName(components, componentCombo.getText());
                parameterCompositeWrapper.setDiscipline(component.getDisciplineForName(newSelection));
                parameterCompositeWrapper.redraw();
            }
        };
    }

    private Listener getTreeViewListener(final Combo componentCombo, final Combo disciplineCombo,
        final ParameterCompositeWrapper parameterCompositeWrapper, final Composite contentComposite) {
        return new Listener() {

            @Override
            public void handleEvent(final Event event) {
                if (viewer.getTree().getSelectionCount() != 1) {
                    return; // to preven empty selection (if possible at all)
                }
                final TreeItem[] items = viewer.getTree().getSelection();
                TreeItem item = items[0];
                String path = item.getText();
                while (item.getParentItem() != null) {
                    item = item.getParentItem();
                    path = item.getText() + "/\\/" + path;
                }
                final String[] parameterBreakDown = path.split("/\\\\/"); // component/discipline/parameter
                if (parameterBreakDown.length < 2) { // no need to change comp/disc/param ( first is empty)
                    return;
                }
                if (!componentCombo.getText().equals(parameterBreakDown[1])) { // we need to switch the component
                    final Component component = getComponentForName(components, parameterBreakDown[1]);
                    componentCombo.select(componentCombo.indexOf(component.getName()));
                    fillDisciplines(disciplineCombo, component);
                    if (parameterBreakDown.length > 2) { // a discipline has been clicked
                        parameterCompositeWrapper.setDiscipline(component.getDisciplineForName(parameterBreakDown[2]));
                        parameterCompositeWrapper.redraw();
                        disciplineCombo.select(disciplineCombo.indexOf(parameterBreakDown[2]));
                    } else { // no discipline is set: take first one
                        parameterCompositeWrapper.setDiscipline(component.getDisciplines().get(0));
                        parameterCompositeWrapper.redraw();
                        disciplineCombo.select(0);
                    }
                } else if (parameterBreakDown.length > 2) {
                    if (!disciplineCombo.getText().equals(parameterBreakDown[2])) {
                        parameterCompositeWrapper.setDiscipline(getComponentForName(components, parameterBreakDown[1])
                            .getDisciplineForName(parameterBreakDown[2]));
                        parameterCompositeWrapper.redraw();
                        disciplineCombo.select(disciplineCombo.indexOf(parameterBreakDown[2]));
                    }
                }
                if (parameterBreakDown.length > 3) { // a parameter name has been clicked
                    for (final Component component : modifiedComponents) {
                        if (component.getName().equals(parameterBreakDown[1])) {
                            final Parameter param =
                                component.getDisciplineForName(parameterBreakDown[2]).getParameterForName(parameterBreakDown[3]);
                            parameterCompositeWrapper.setName(param.getName());
                            parameterCompositeWrapper.setDescription(param.getDescription());
                            parameterCompositeWrapper.setValue(param.getValue());
                            parameterCompositeWrapper.setFactor(param.getFactor());
                        }
                    }
                    final org.eclipse.swt.widgets.List paramList =
                        (org.eclipse.swt.widgets.List) ((Composite) contentComposite.getChildren()[0]).getChildren()[0];
                    for (int i = 0, n = paramList.getItemCount(); i < n; i++) { // find the correct
                                                                                // parameter in the
                                                                                // list
                        if (paramList.getItem(i).equals(parameterBreakDown[3])) {
                            paramList.setSelection(i);
                            break;
                        }
                    }
                }
            }
        };
    }

    private KeyListener getKeyAdapter() {
        return new KeyAdapter() {

            @Override
            public void keyReleased(final KeyEvent event) {
                if (event.character == SWT.DEL) {
                    if (viewer.getTree().getSelectionCount() != 1) {
                        return; // to preven empty selection (if possible at all)
                    }
                    final TreeItem[] items = viewer.getTree().getSelection();
                    TreeItem item = items[0];
                    String path = item.getText();
                    while (item.getParentItem() != null) {
                        item = item.getParentItem();
                        path = item.getText() + "/\\/" + path;
                    }
                    final String[] parameterBreakDown = path.split("/\\\\/"); // component/discipline/parameter
                    if (parameterBreakDown.length < 4) { // only remove params
                        return;
                    }
                    final Discipline discipline = getComponentForName(modifiedComponents, parameterBreakDown[1])
                        .getDisciplineForName(parameterBreakDown[2]);
                    discipline.getParameters().remove(discipline.getParameterForName(parameterBreakDown[3]));
                    if (discipline.getParameters().size() == 0) {
                        discipline.getComponent().getDisciplines().remove(discipline);
                        if (discipline.getComponent().getDisciplines().size() == 0) {
                            modifiedComponents.remove(discipline.getComponent());
                        }
                    }
                    viewer.refresh();
                }
            }
        };
    }

    /**
     * Helper to update the drop-down box.
     * 
     * @param disciplineCombo The combo
     * @param component The parent component
     */
    private void fillDisciplines(final Combo disciplineCombo, final Component component) {
        disciplineCombo.removeAll();
        for (final Discipline discipline : component.getDisciplines()) {
            disciplineCombo.add(discipline.getName());
        }
    }

    /**
     * Call this with an existing created configuration to set the GUI contents accordingly. The routine doesn't allow non-existing entries
     * of the initially loaded GUI definition file.
     * 
     * @param configuration The configuration to parse as one XML string.
     */
    public void setSelectedParameters(final String configuration) {
        try {
            final List<Component> setComponents = guiInputParser.parse(configuration);
            modifiedComponents.clear();
            for (final Component setComponent : setComponents) {
                final Component compDefinition = getComponentForName(components, setComponent.getName());
                if (compDefinition == null) {
                    LOGGER.warn("Ignoring component setting " + setComponent.getName());
                    continue; // skip loaded setting, because it's not allowed
                }
                if (getComponentForName(modifiedComponents, setComponent.getName()) != null) {
                    LOGGER.warn("Ignoring duplicate component setting " + setComponent.getName());
                    continue;
                }
                final Component newComponent = new Component();
                newComponent.setName(setComponent.getName());
                modifiedComponents.add(newComponent);
                for (final Discipline setDiscipline : setComponent.getDisciplines()) {
                    final Discipline discDefinition = compDefinition.getDisciplineForName(setDiscipline.getName());
                    if (discDefinition == null) {
                        LOGGER.warn("Ignoring discipline setting " + setDiscipline.getName());
                        continue;
                    }
                    if (newComponent.getDisciplineForName(setDiscipline.getName()) != null) {
                        LOGGER.warn("Ignoring duplicate discipline setting " + setDiscipline.getName());
                        continue;
                    }
                    final Discipline newDiscipline = new Discipline();
                    newDiscipline.setName(setDiscipline.getName());
                    newComponent.getDisciplines().add(newDiscipline);
                    for (final Parameter setParameter : setDiscipline.getParameters()) {
                        final Parameter paramDefinition = discDefinition.getParameterForName(setParameter.getName());
                        if (paramDefinition == null) {
                            LOGGER.warn("Ignoring parameter setting " + setParameter.getName());
                            continue;
                        }
                        if (newDiscipline.getParameterForName(setParameter.getName()) != null) {
                            LOGGER.warn("Ignoring duplicate parameter setting " + setParameter.getName());
                            continue;
                        }
                        final Parameter newParameter = new Parameter(setParameter);
                        newParameter.setName(setParameter.getName());
                        newDiscipline.getParameters().add(newParameter);
                    }
                }

            }
            viewer.refresh();
            viewer.expandAll();
        } catch (final XPathExpressionException | XMLException e) {
            LOGGER.error("Error parsing earlier vampzero configuration", e);
        }
    }

    /**
     * Helper symmetrical to the component and discipline method helpers.
     * 
     * @param comp The list of all loaded components
     * @param name The name to look for
     * @return The component with the given name or null if not found
     */
    private Component getComponentForName(final List<Component> comp, final String name) {
        assert comp != null;
        assert comp.size() > 0;
        assert name != null;
        for (final Component component : comp) {
            if (component.getName().equals(name)) {
                return component;
            }
        }
        return null; // nothing found throws exception, shouldn't ever happen
    }

}
