/*
 * OldClockModelsPanel.java
 *
 * Copyright (c) 2002-2015 Alexei Drummond, Andrew Rambaut and Marc Suchard
 *
 * This file is part of BEAST.
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership and licensing.
 *
 * BEAST is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 *  BEAST is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with BEAST; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

package dr.app.beauti.clockModelsPanel;

import dr.app.beauti.BeautiFrame;
import dr.app.beauti.BeautiPanel;
import dr.app.beauti.ComboBoxRenderer;
import dr.app.beauti.options.BeautiOptions;
import dr.app.beauti.options.ClockModelGroup;
import dr.app.beauti.options.PartitionClockModel;
import dr.app.beauti.types.OldClockType;
import dr.app.gui.table.RealNumberCellEditor;
import dr.app.gui.table.TableEditorStopper;
import dr.evolution.datatype.DataType;
import jam.framework.Exportable;
import jam.panels.ActionPanel;
import jam.table.TableRenderer;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.plaf.BorderUIResource;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Andrew Rambaut
 * @author Alexei Drummond
 * @author Walter Xie
 * @version $Id: ClockModelPanel.java,v 1.17 2006/09/05 13:29:34 rambaut Exp $
 */
public class OldClockModelsPanel extends BeautiPanel implements Exportable {

    private static final long serialVersionUID = 2945922234432540027L;
    private final String[] columnToolTips = {"Name", "Clock model",
            "Decide whether to estimate this clock model",
            "Provide the rate if it is fixed",
            "The group which the clock model is belonging to"};
    private final String[] columnToolTips2 = {"A group of clock models",
            "<html>Fix mean rate of this group of clock models." +
                    "<br>Select this option to fix the mean substitution rate,<br>" +
                    "rather than try to infer it. If this option is turned off, then<br>" +
                    "either the sequences should have dates or the tree should have<br>" +
                    "sufficient calibration informations specified as priors.<br>" +
                    "In addition, it is only available for multi-clock partitions.</html>",
            "Enter the fixed mean rate here."};
    private static final int MINIMUM_TABLE_HEIGHT = 400;

    JTable clockModelTable = null;
    ClockModelTableModel clockModelTableModel = null;
    JScrollPane scrollPane;
//    JCheckBox fixedMeanRateCheck = new JCheckBox("Fix mean rate of molecular clock model to: ");
//    RealNumberField meanRateField = new RealNumberField(Double.MIN_VALUE, Double.MAX_VALUE);

    JTable clockGroupTable = null;
    ClockGroupTableModel clockGroupTableModel = null;

    BeautiFrame frame = null;
    BeautiOptions options = null;
    boolean settingOptions = false;

    public List<ClockModelGroup> clockModelGroupList = new ArrayList<ClockModelGroup>();

    public OldClockModelsPanel(BeautiFrame parent) {

        this.frame = parent;

        clockModelTableModel = new ClockModelTableModel();
        clockModelTable = new JTable(clockModelTableModel) {
            //Implement table header tool tips.
            protected JTableHeader createDefaultTableHeader() {
                return new JTableHeader(columnModel) {
                    public String getToolTipText(MouseEvent e) {
                        Point p = e.getPoint();
                        int index = columnModel.getColumnIndexAtX(p.x);
                        int realIndex = columnModel.getColumn(index).getModelIndex();
                        return columnToolTips[realIndex];
                    }
                };
            }
        };

        initTable(clockModelTable);

        scrollPane = new JScrollPane(clockModelTable,
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setOpaque(false);

        // PanelUtils.setupComponent(clockModelCombo);
        // clockModelCombo.setToolTipText("<html>Select either a strict molecular clock or<br>or a relaxed clock model.</html>");
        // clockModelCombo.addItemListener(comboListener);

//        PanelUtils.setupComponent(fixedMeanRateCheck);
//        fixedMeanRateCheck.setSelected(false); // default to FixRateType.ESTIMATE
//        fixedMeanRateCheck.addItemListener(new ItemListener() {
//            public void itemStateChanged(ItemEvent ev) {
//                meanRateField.setEnabled(fixedMeanRateCheck.isSelected());
//                if (fixedMeanRateCheck.isSelected()) {
//                    options.clockModelOptions.fixMeanRate();
//                } else {
//                    options.clockModelOptions.fixRateOfFirstClockPartition();
//                }
//
//                clockModelTableModel.fireTableDataChanged();
//                fireModelsChanged();
//            }
//        });
//        fixedMeanRateCheck.setToolTipText("<html>Select this option to fix the mean substitution rate,<br>"
//                + "rather than try to infer it. If this option is turned off, then<br>"
//                + "either the sequences should have dates or the tree should have<br>"
//                + "sufficient calibration informations specified as priors.<br>"
//                + "In addition, it is only available for multi-clock partitions." + "</html>");// TODO Alexei
//
//        PanelUtils.setupComponent(meanRateField);
//        meanRateField.setEnabled(fixedMeanRateCheck.isSelected());
//        meanRateField.setValue(1.0);
//        meanRateField.addKeyListener(new java.awt.event.KeyAdapter() {
//            public void keyTyped(java.awt.event.KeyEvent ev) {
//                frame.setDirty();
//            }
//        });
//        meanRateField.setToolTipText("<html>Enter the fixed mean rate here.</html>");
//        meanRateField.setColumns(10);
//		meanRateField.setEnabled(true);

        JPanel modelPanelParent = new JPanel(new BorderLayout(12, 12));
//        modelPanelParent.setLayout(new BoxLayout(modelPanelParent, BoxLayout.Y_AXIS));
        modelPanelParent.setOpaque(false);
        TitledBorder modelBorder = new TitledBorder("Clock Model : ");
        modelPanelParent.setBorder(modelBorder);

//        OptionsPanel panel = new OptionsPanel(12, 12);
//        panel.addComponents(fixedMeanRateCheck, meanRateField);


        // The bottom panel is now small enough that this is not necessary
//        JScrollPane scrollPane2 = new JScrollPane(panel);
//        scrollPane2.setOpaque(false);
//        scrollPane2.setPreferredSize(new Dimension(400, 150));

        modelPanelParent.add(scrollPane, BorderLayout.CENTER);
//        modelPanelParent.add(panel, BorderLayout.SOUTH);

        //=======  Clock Model Group for Fix Mean function ==========
        clockGroupTableModel = new ClockGroupTableModel();
        clockGroupTable = new JTable(clockGroupTableModel) {
            //Implement table header tool tips.
            protected JTableHeader createDefaultTableHeader() {
                return new JTableHeader(columnModel) {
                    public String getToolTipText(MouseEvent e) {
                        Point p = e.getPoint();
                        int index = columnModel.getColumnIndexAtX(p.x);
                        int realIndex = columnModel.getColumn(index).getModelIndex();
                        return columnToolTips2[realIndex];
                    }
                };
            }
        };
        clockGroupTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        clockGroupTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        clockGroupTable.getTableHeader().setReorderingAllowed(false);

        TableColumn col = clockGroupTable.getColumnModel().getColumn(0);
        col.setMinWidth(200);
        col = clockGroupTable.getColumnModel().getColumn(1);
        col.setMinWidth(40);
        col.setCellRenderer(new GrayableCheckboxCellRenderer());
        col = clockGroupTable.getColumnModel().getColumn(2);
        col.setCellEditor(new RealNumberCellEditor(0, Double.POSITIVE_INFINITY));
        col.setMinWidth(80);
        TableEditorStopper.ensureEditingStopWhenTableLosesFocus(clockGroupTable);

        JScrollPane d_scrollPane = new JScrollPane(clockGroupTable,
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        d_scrollPane.setOpaque(false);

        ActionPanel actionPanel1 = new ActionPanel(false);
        actionPanel1.setAddAction(addClockGroupAction);
        actionPanel1.setRemoveAction(removeClockGroupAction);
        addClockGroupAction.setEnabled(false);
        removeClockGroupAction.setEnabled(false);

        JPanel groupPanel = new JPanel(new BorderLayout(12, 12));
        groupPanel.add(d_scrollPane, BorderLayout.CENTER);
        groupPanel.add(actionPanel1, BorderLayout.SOUTH);
        TitledBorder traitClockBorder = new TitledBorder("Clock Model Group: ");
        groupPanel.setBorder(traitClockBorder);

        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, modelPanelParent, groupPanel);
        splitPane.setDividerLocation(MINIMUM_TABLE_HEIGHT);

        setOpaque(false);
        setLayout(new BorderLayout(12, 12));
        setBorder(new BorderUIResource.EmptyBorderUIResource(new Insets(12, 12, 12, 12)));
        add(splitPane, BorderLayout.CENTER);
    }

    Action addClockGroupAction = new AbstractAction("+") {
        public void actionPerformed(ActionEvent ae) {
            String s = JOptionPane.showInputDialog(frame,
                    "Please input the new group name below:",
                    "Add A New Clock Model Group Dialog",
                    JOptionPane.PLAIN_MESSAGE, null, null,
                    clockModelGroupList.size() + "_group").toString().trim();

            if ((s != null) && (s.length() > 0)) {
                if (options.clockModelOptions.containsGroup(s, clockModelGroupList)) {
                    errorMessageDialog("This name has been used already,\nplease input a new name.");
                } else {
                    clockModelGroupList.add(new ClockModelGroup(s));
                    modelsChanged();
                }

            } else {
                errorMessageDialog("Please input a name properly.");
            }
        }
    };

    Action removeClockGroupAction = new AbstractAction("-") {
        public void actionPerformed(ActionEvent ae) {
            int row = clockGroupTable.getSelectedRow();
            if (row >= 0) {
                ClockModelGroup group = clockModelGroupList.get(row);
                if (options.getPartitionClockModels(group).size() > 0) {
                    errorMessageDialog("Cannot remove the group " + group.getName() +
                            ",\nwhich contains clock model(s)." +
                            "\nPlease assign model(s) to a different group first.");
                } else {
                    clockModelGroupList.remove(row);
                    modelsChanged();
                }
            } else {
                errorMessageDialog("Please select a group properly.");
            }
        }
    };

    private void errorMessageDialog(String e) {
        JOptionPane.showMessageDialog(this, e, "Clock Model Panel Error", JOptionPane.ERROR_MESSAGE);
    }

    private void initTable(JTable dataTable) {
        dataTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        dataTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        dataTable.getTableHeader().setReorderingAllowed(false);
//        clockModelTable.getTableHeader().setDefaultRenderer(
//              new HeaderRenderer(SwingConstants.LEFT, new Insets(0, 4, 0, 4)));

        TableColumn col = dataTable.getColumnModel().getColumn(0);
        col.setCellRenderer(new ClockTableCellRenderer(SwingConstants.LEFT, new Insets(0, 4, 0, 4)));
        col.setMinWidth(200);

        col = dataTable.getColumnModel().getColumn(1);
        ComboBoxRenderer comboBoxRenderer = new ComboBoxRenderer();
        comboBoxRenderer.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
        col.setCellRenderer(comboBoxRenderer);
        col.setMinWidth(260);

        col = dataTable.getColumnModel().getColumn(2);
        col.setMinWidth(40);
        col.setCellRenderer(new GrayableCheckboxCellRenderer());

        col = dataTable.getColumnModel().getColumn(3);
        col.setCellRenderer(new ClockTableCellRenderer(SwingConstants.LEFT, new Insets(0, 4, 0, 4)));
        col.setCellEditor(new RealNumberCellEditor(0, Double.POSITIVE_INFINITY));
        col.setMinWidth(80);

        col = dataTable.getColumnModel().getColumn(4);
        col.setCellRenderer(comboBoxRenderer);
        col.setMinWidth(200);

        TableEditorStopper.ensureEditingStopWhenTableLosesFocus(dataTable);
    }

    private void modelsChanged() {
        TableColumn col = clockModelTable.getColumnModel().getColumn(1);
        col.setCellEditor(new DefaultCellEditor(new JComboBox(new OldClockType[]{
                OldClockType.STRICT_CLOCK,
                OldClockType.UNCORRELATED_LOGNORMAL,
//                OldClockType.UNCORRELATED_CAUCHY,
//                OldClockType.UNCORRELATED_GAMMA,
                OldClockType.UNCORRELATED_EXPONENTIAL,
//                OldClockType.AUTOCORRELATED
                OldClockType.RANDOM_LOCAL_CLOCK,
                OldClockType.FIXED_LOCAL_CLOCK
        })));

        col = clockModelTable.getColumnModel().getColumn(4);
        col.setCellEditor(new DefaultCellEditor(new JComboBox(options.clockModelOptions.getClockModelGroupNames(clockModelGroupList))));

        addClockGroupAction.setEnabled(clockModelGroupList.size() > 0);
        removeClockGroupAction.setEnabled(clockModelGroupList.size() > 1);

        clockGroupTableModel.fireTableDataChanged();
    }

    private void fireModelsChanged() {
        options.updatePartitionAllLinks();
        clockModelTableModel.fireTableDataChanged();
        clockGroupTableModel.fireTableDataChanged();
        frame.setStatusMessage();
        frame.setDirty();
    }

//    private void updateModelPanelBorder() {
//    	if (options.hasData()) {
//    		modelBorder.setTitle(options.clockModelOptions.getRateOptionClockModel().toString());
//    	} else {
//    		modelBorder.setTitle("Overall clock model(s) parameters");
//    	}
//
//        repaint();
//    }

    public void setOptions(BeautiOptions options) {

        this.options = options;

        settingOptions = true;

        clockModelGroupList = options.clockModelOptions.getClockModelGroups();
        addClockGroupAction.setEnabled(clockModelGroupList.size() > 0);
        removeClockGroupAction.setEnabled(clockModelGroupList.size() > 1);

//        fixedMeanRateCheck.setSelected(options.clockModelOptions.getRateOptionClockModel() == FixRateType.FIX_MEAN);
//        fixedMeanRateCheck.setEnabled(!(options.clockModelOptions.getRateOptionClockModel() == FixRateType.TIP_CALIBRATED
//                || options.clockModelOptions.getRateOptionClockModel() == FixRateType.NODE_CALIBRATED
//                || options.clockModelOptions.getRateOptionClockModel() == FixRateType.RATE_CALIBRATED));
//        meanRateField.setValue(options.clockModelOptions.getMeanRelativeRate());

        settingOptions = false;

        int selRow = clockModelTable.getSelectedRow();
        clockModelTableModel.fireTableDataChanged();
        if (options.getPartitionClockModels().size() > 0) {
            if (selRow < 0) {
                selRow = 0;
            }
            clockModelTable.getSelectionModel().setSelectionInterval(selRow, selRow);
        }

//        fireModelsChanged();

        modelsChanged();

        clockModelTableModel.fireTableDataChanged();
    }

    public void getOptions(BeautiOptions options) {
        if (settingOptions) return;

//        if (fixedMeanRateCheck.isSelected()) {
//        	options.clockModelOptions.fixMeanRate();
//        } else {
//        	options.clockModelOptions.fixRateOfFirstClockPartition();
//        }
//        options.clockModelOptions.setMeanRelativeRate(meanRateField.getValue());

//        fireModelsChanged();
    }

    public JComponent getExportableComponent() {
        return clockModelTable;
    }

    class ClockModelTableModel extends AbstractTableModel {

        private static final long serialVersionUID = -2852144669936634910L;

        //        String[] columnNames = {"Clock Model Name", "Molecular Clock Model"};
        final private String[] columnNames = new String[]{"Name", "Model", "Estimate", "Rate", "Group"};

        public ClockModelTableModel() {
        }

        public int getColumnCount() {
            return columnNames.length;
        }

        public int getRowCount() {
            if (options == null) return 0;
            return options.getPartitionClockModels().size();
        }

        public Object getValueAt(int row, int col) {
            PartitionClockModel model = options.getPartitionClockModels().get(row);
            switch (col) {
                case 0:
                    return model.getName();
                case 1:
                    return OldClockType.getType(model.getClockType(), model.getClockDistributionType());
                case 2:
                    return model.isEstimatedRate();
                case 3:
                    return model.getRate();
                case 4:
                    return model.getClockModelGroup().getName();
            }
            return null;
        }

        public void setValueAt(Object aValue, int row, int col) {
            PartitionClockModel model = options.getPartitionClockModels().get(row);
            switch (col) {
                case 0:
                    String name = ((String) aValue).trim();
                    if (name.length() > 0) {
                        model.setName(name);
                    }
                    break;
                case 1:
                    OldClockType type = (OldClockType) aValue;
                    model.setClockType(type.getClockType());
                    model.setClockDistributionType(type.getClockDistributionType());
                    break;
                case 2:
                    model.setEstimatedRate((Boolean) aValue);
//                    if (options.clockModelOptions.getRateOptionClockModel() == FixRateType.RElATIVE_TO) {
//                        if (!options.clockModelOptions.validateRelativeTo()) {
//                            JOptionPane.showMessageDialog(frame, "It must have at least one clock rate to be fixed !",
//                                    "Validation Of Relative To ?th Rate", JOptionPane.WARNING_MESSAGE);
//                            model.setEstimatedRate(false);
//                        }
//                    }
                    break;
                case 3:
                    model.setRate((Double) aValue, true);
//                    options.selectParameters();
                    break;
                case 4:
                    model.setClockModelGroup(options.clockModelOptions.getGroup(aValue.toString(), clockModelGroupList));
                    break;
                default:
                    throw new IllegalArgumentException("unknown column, " + col);
            }
            fireModelsChanged();
        }

        public boolean isCellEditable(int row, int col) {
            boolean editable;
            PartitionClockModel model = options.getPartitionClockModels().get(row);
            ClockModelGroup group = model.getClockModelGroup();
            switch (col) {
                case 1:
                    editable = !(model.getDataType().getType() == DataType.MICRO_SAT ||
                            model.getDataType().getType() == DataType.GENERAL);
                    break;
                case 2:// Check box
                case 4:
                    editable = !group.isFixMean();
                    break;
                case 3:
                    editable = !group.isFixMean();// && !((Boolean) getValueAt(row, 2));
                    break;
                default:
                    editable = true;
            }

            return editable;
        }

        public String getColumnName(int column) {
            return columnNames[column];
        }

        public Class getColumnClass(int c) {
            if (getRowCount() == 0) {
                return Object.class;
            }
            return getValueAt(0, c).getClass();
        }

        public String toString() {
            StringBuffer buffer = new StringBuffer();

            buffer.append(getColumnName(0));
            for (int j = 1; j < getColumnCount(); j++) {
                buffer.append("\t");
                buffer.append(getColumnName(j));
            }
            buffer.append("\n");

            for (int i = 0; i < getRowCount(); i++) {
                buffer.append(getValueAt(i, 0));
                for (int j = 1; j < getColumnCount(); j++) {
                    buffer.append("\t");
                    buffer.append(getValueAt(i, j));
                }
                buffer.append("\n");
            }

            return buffer.toString();
        }
    }

    class ClockTableCellRenderer extends TableRenderer {

        public ClockTableCellRenderer(int alignment, Insets insets) {
            super(alignment, insets);
        }

        public Component getTableCellRendererComponent(JTable aTable,
                                                       Object value,
                                                       boolean aIsSelected,
                                                       boolean aHasFocus,
                                                       int aRow, int aColumn) {

            if (value == null) return this;

            Component renderer = super.getTableCellRendererComponent(aTable,
                    value,
                    aIsSelected,
                    aHasFocus,
                    aRow, aColumn);

            ClockModelGroup group = options.getPartitionClockModels().get(aRow).getClockModelGroup();
            if (group.isFixMean() && aColumn > 1) {
                renderer.setForeground(Color.gray);
//            } else if (!group.isFixMean() && aColumn == 3) {
//                renderer.setForeground(Color.gray);
            } else {
                renderer.setForeground(Color.black);
            }

            return this;
        }

    }

    public class GrayableCheckboxCellRenderer extends JCheckBox implements TableCellRenderer {
        public Component getTableCellRendererComponent(JTable table, Object value,
                                 boolean isSelected, boolean hasFocus, int vRowIndex, int vColIndex) {
            boolean editable = table.getModel().isCellEditable(vRowIndex, vColIndex);
            setBackground(editable ? Color.WHITE : Color.LIGHT_GRAY);
            setEnabled(editable);
            setSelected((Boolean) value);
            setHorizontalAlignment(SwingConstants.CENTER);
            return this;
        }
    }

    class ClockGroupTableModel extends AbstractTableModel {

        String[] columnNames = {"Group Name", "Fix Mean", "Rate"};

        public ClockGroupTableModel() {
        }

        public int getColumnCount() {
            return columnNames.length;
        }

        public int getRowCount() {
            if (options == null) return 0;
//            return options.clockModelOptions.getClockModelGroups().size();
            return clockModelGroupList.size();
        }

        public Object getValueAt(int row, int col) {
            switch (col) {
                case 0:
                    return clockModelGroupList.get(row).getName();
                case 1:
                    return clockModelGroupList.get(row).isFixMean();
                case 2:
                    if (!clockModelGroupList.get(row).isFixMean()) return "";
                    return clockModelGroupList.get(row).getFixMeanRate();
            }
            return null;
        }

        public void setValueAt(Object aValue, int row, int col) {

            switch (col) {
                case 0:
                    String name = ((String) aValue).trim();
                    if (name.length() > 0) {
                        clockModelGroupList.get(row).setName(name);
                        modelsChanged();
                    }
                    break;
                case 1:
                    ClockModelGroup group = clockModelGroupList.get(row);
                    group.setFixMean((Boolean) aValue);
                    if ((Boolean) aValue) {
                        options.clockModelOptions.fixMeanRate(group);
                    } else {
                        options.clockModelOptions.fixRateOfFirstClockPartition(group);
                    }
                    break;
                case 2:
                    clockModelGroupList.get(row).setFixMeanRate((Double) aValue, options);
                    break;

                default:
                    throw new IllegalArgumentException("unknown column, " + col);
            }

            fireModelsChanged();
        }

        public boolean isCellEditable(int row, int col) {
            switch (col) {
                case 1:// Check box
                    return options.getPartitionClockModels(options.clockModelOptions.
                            getGroup(getValueAt(row, 0).toString(), clockModelGroupList)).size() > 1;
                case 2:
                    return /*!fixedMeanRateCheck.isSelected() &&*/ ((Boolean) getValueAt(row, 1));
                default:
                    return true;
            }
        }

        public String getColumnName(int column) {
            return columnNames[column];
        }

        public Class getColumnClass(int c) {
            if (getRowCount() == 0) {
                return Object.class;
            }
            return getValueAt(0, c).getClass();
        }

        public String toString() {
            StringBuffer buffer = new StringBuffer();

            buffer.append(getColumnName(0));
            for (int j = 1; j < getColumnCount(); j++) {
                buffer.append("\t");
                buffer.append(getColumnName(j));
            }
            buffer.append("\n");

            for (int i = 0; i < getRowCount(); i++) {
                buffer.append(getValueAt(i, 0));
                for (int j = 1; j < getColumnCount(); j++) {
                    buffer.append("\t");
                    buffer.append(getValueAt(i, j));
                }
                buffer.append("\n");
            }

            return buffer.toString();
        }
    }

}