/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *    https://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 org.apache.cayenne.modeler.editor;

import org.apache.cayenne.configuration.event.QueryEvent;
import org.apache.cayenne.map.Attribute;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.QueryDescriptor;
import org.apache.cayenne.map.Relationship;
import org.apache.cayenne.map.SQLTemplateDescriptor;
import org.apache.cayenne.modeler.Application;
import org.apache.cayenne.modeler.ProjectController;
import org.apache.cayenne.modeler.undo.AddPrefetchUndoableEditForSqlTemplate;
import org.apache.cayenne.modeler.util.CayenneAction;
import org.apache.cayenne.modeler.util.EntityTreeFilter;
import org.apache.cayenne.modeler.util.EntityTreeModel;
import org.apache.cayenne.modeler.util.ModelerUtil;
import org.apache.cayenne.modeler.util.MultiColumnBrowser;
import org.apache.cayenne.modeler.util.UIUtil;
import org.apache.cayenne.swing.components.image.FilteredIconFactory;
import org.apache.cayenne.util.CayenneMapEntry;

import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.ListSelectionModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.tree.TreeModel;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.prefs.Preferences;

/**
 * Class configured to work with prefetches.
 */
public class SQLTemplatePrefetchTab extends JPanel implements PropertyChangeListener {

    // property for split pane divider size
    private static final String SPLIT_DIVIDER_LOCATION_PROPERTY = "query.orderings.divider.location";

    private static final Dimension BROWSER_CELL_DIM = new Dimension(150, 100);
    private static final Dimension TABLE_DIM = new Dimension(460, 60);

    private static final String REAL_PANEL = "real";
    private static final String PLACEHOLDER_PANEL = "placeholder";

    protected ProjectController mediator;
    protected SQLTemplateDescriptor sqlTemplate;

    protected MultiColumnBrowser browser;
    protected JTable table;

    protected CardLayout cardLayout;
    protected JPanel messagePanel;

    public SQLTemplatePrefetchTab(ProjectController mediator) {
        this.mediator = mediator;

        initView();
        initController();
    }

    protected void initView() {
        messagePanel = new JPanel(new BorderLayout());
        cardLayout = new CardLayout();

        Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), "");

        int defLocation = Application.getFrame().getHeight() / 2;
        int location = detail != null ? detail.getInt(
                getDividerLocationProperty(),
                defLocation) : defLocation;

        //As of CAY-888 #3 main pane is now a JSplitPane. Top component is a bit larger.
        JSplitPane mainPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        mainPanel.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);
        mainPanel.setDividerLocation(location);

        mainPanel.setTopComponent(createEditorPanel());
        mainPanel.setBottomComponent(createSelectorPanel());

        setLayout(cardLayout);
        add(mainPanel, REAL_PANEL);
        add(messagePanel, PLACEHOLDER_PANEL);
    }

    protected void initController() {
        // scroll to selected row whenever a selection even occurs
        table.getSelectionModel().addListSelectionListener(e -> {
            if (!e.getValueIsAdjusting()) {
                UIUtil.scrollToSelectedRow(table);
            }
        });
    }

    protected void initFromModel() {
        QueryDescriptor query = mediator.getCurrentQuery();

        if (query == null || !QueryDescriptor.SQL_TEMPLATE.equals(query.getType())) {
            processInvalidModel("Unknown query.");
            return;
        }

        if (!(query.getRoot() instanceof Entity)) {
            processInvalidModel("SQLTemplate has no root set.");
            return;
        }

        this.sqlTemplate = (SQLTemplateDescriptor) query;
        browser.setModel(createBrowserModel((Entity<?,?,?>) sqlTemplate.getRoot()));

        table.setModel(createTableModel());
        setUpPrefetchBox(table.getColumnModel().getColumn(2));

        // init column sizes
        table.getColumnModel().getColumn(0).setPreferredWidth(250);

        cardLayout.show(this, REAL_PANEL);
    }

    protected void processInvalidModel(String message) {
        JLabel messageLabel = new JLabel(message, JLabel.CENTER);

        messagePanel.removeAll();
        messagePanel.add(messageLabel, BorderLayout.CENTER);
        cardLayout.show(this, PLACEHOLDER_PANEL);
    }

    protected void setUpPrefetchBox(TableColumn column) {

        JComboBox<String> prefetchBox = new JComboBox<>();
        prefetchBox.addItem(SelectQueryPrefetchTab.JOINT_PREFETCH_SEMANTICS);
        prefetchBox.addItem(SelectQueryPrefetchTab.DISJOINT_BY_ID_PREFETCH_SEMANTICS);

        prefetchBox.addActionListener(e -> Application.getInstance().getFrameController().getEditorView().getEventController().setDirty(true));

        column.setCellEditor(new DefaultCellEditor(prefetchBox));

        DefaultTableCellRenderer renderer =
                new DefaultTableCellRenderer();
        renderer.setToolTipText("Click for combo box");
        column.setCellRenderer(renderer);
    }

    protected JPanel createEditorPanel() {

        table = new JTable();
        table.setRowHeight(25);
        table.setRowMargin(3);
        table.setPreferredScrollableViewportSize(TABLE_DIM);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        JPanel panel = new JPanel(new BorderLayout());
        panel.add(new JScrollPane(table), BorderLayout.CENTER);

        return panel;
    }

    protected JPanel createSelectorPanel() {

        browser = new MultiColumnBrowser();
        browser.setPreferredColumnSize(BROWSER_CELL_DIM);
        browser.setDefaultRenderer();

        JPanel panel = new JPanel(new BorderLayout());
        panel.add(createToolbar(), BorderLayout.NORTH);
        panel.add(new JScrollPane(
                browser,
                JScrollPane.VERTICAL_SCROLLBAR_NEVER,
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), BorderLayout.CENTER);

        // setting minimal size, otherwise scrolling looks awful, because of
        // VERTICAL_SCROLLBAR_NEVER strategy
        panel.setMinimumSize(panel.getPreferredSize());

        return panel;
    }

    protected JComponent createToolbar() {

        JButton add = new CayenneAction.CayenneToolbarButton(null, 1);
        add.setText("Add Prefetch");
        Icon addIcon = ModelerUtil.buildIcon("icon-plus.png");
        add.setIcon(addIcon);
        add.setDisabledIcon(FilteredIconFactory.createDisabledIcon(addIcon));

        add.addActionListener(e -> {
            String prefetch = getSelectedPath();

            if (prefetch == null) {
                return;
            }

            addPrefetch(prefetch);

            Application.getInstance().getUndoManager().addEdit(new AddPrefetchUndoableEditForSqlTemplate(prefetch, SQLTemplatePrefetchTab.this));
        });

        JButton remove = new CayenneAction.CayenneToolbarButton(null, 3);
        remove.setText("Remove Prefetch");
        Icon removeIcon = ModelerUtil.buildIcon("icon-trash.png");
        remove.setIcon(removeIcon);
        remove.setDisabledIcon(FilteredIconFactory.createDisabledIcon(removeIcon));
        remove.addActionListener(e -> {
            int selection = table.getSelectedRow();
            if (selection < 0) {
                return;
            }
            String prefetch = (String) table.getModel().getValueAt(selection, 0);
            removePrefetch(prefetch);
        });

        JToolBar toolBar = new JToolBar();
        toolBar.setBorder(BorderFactory.createEmptyBorder());
        toolBar.setFloatable(false);
        toolBar.add(add);
        toolBar.add(remove);
        return toolBar;
    }

    protected String getSelectedPath() {

        Object[] path = browser.getSelectionPath().getPath();

        // first item in the path is Entity, so we must have
        // at least two elements to constitute a valid ordering path
        if (path.length < 2) {
            return null;
        }

        StringBuilder buffer = new StringBuilder();

        // attribute or relationships
        CayenneMapEntry first = (CayenneMapEntry) path[1];
        buffer.append(first.getName());

        for (int i = 2; i < path.length; i++) {
            CayenneMapEntry pathEntry = (CayenneMapEntry) path[i];
            buffer.append(".").append(pathEntry.getName());
        }

        return buffer.toString();
    }

    protected TreeModel createBrowserModel(Entity<?,?,?> entity) {

        EntityTreeModel treeModel = new EntityTreeModel(entity);
        treeModel.setFilter(
                new EntityTreeFilter() {
                    public boolean attributeMatch(Object node, Attribute<?,?,?> attr) {
                        return false;
                    }

                    public boolean relationshipMatch(Object node, Relationship<?,?,?> rel) {
                        return true;
                    }
                });
        return treeModel;
    }

    protected TableModel createTableModel() {
        return new PrefetchModel(sqlTemplate.getPrefetchesMap(), sqlTemplate.getRoot());
    }

    public void addPrefetch(String prefetch) {

        // check if such prefetch already exists
        if (!sqlTemplate.getPrefetchesMap().isEmpty() && sqlTemplate.getPrefetchesMap().containsKey(prefetch)) {
            return;
        }

        //default value is joint
        sqlTemplate.addPrefetch(prefetch, PrefetchModel.getPrefetchType(SelectQueryPrefetchTab.DISJOINT_BY_ID_PREFETCH_SEMANTICS));

        // reset the model, since it is immutable
        table.setModel(createTableModel());
        setUpPrefetchBox(table.getColumnModel().getColumn(2));

        mediator.fireQueryEvent(new QueryEvent(this, sqlTemplate));
    }

    public void removePrefetch(String prefetch) {
        sqlTemplate.removePrefetch(prefetch);

        // reset the model, since it is immutable
        table.setModel(createTableModel());
        setUpPrefetchBox(table.getColumnModel().getColumn(2));

        mediator.fireQueryEvent(new QueryEvent(this, sqlTemplate));
    }

    /**
     * Updates split pane divider location in properties
     */
    public void propertyChange(PropertyChangeEvent evt) {
        if (JSplitPane.DIVIDER_LOCATION_PROPERTY.equals(evt.getPropertyName())) {
            int value = (Integer) evt.getNewValue();

            Preferences detail = Application.getInstance().getPreferencesNode(this.getClass(), "");
            detail.putInt(getDividerLocationProperty(), value);
        }
    }

    /**
     * Returns name of a property for divider location.
     */
    protected String getDividerLocationProperty() {
        return SPLIT_DIVIDER_LOCATION_PROPERTY;
    }

}
