package vermilion;

// TODO: Remember the last shown pattern for each machine.

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.font.FontRenderContext;
import java.util.ArrayList;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;



// TODO: Understand nested vs private classes, gulp!
/**
 * Table for patterns, a heavily modified JTable made to work like a tracker.
 */
public class JPatternTable extends JTable
{
    // A cell renderer for displaying tracker column cells, which are just text.
    private JPatternTableCellRenderer cellRenderer;

    // The composer system component that owns this control.
    private SystemComposerPatternData sysComposer;

    // The tracker panel of which this control is part.
    private JTrackerPanel trackerPanel;
    private JPanel backPanel;

    // Indicates that something has been changed in the currently showing pattern.
    protected boolean currPatternChanged = false;

    // Since we may get out of sync during update operations, we have our own
    // current machine and pattern pointers.
    public PatternNode currPattern = null;
    public MachineNode currMachine = null;
    public JTrackerColumn currColumn = null;

    // The time signature of the pattern currently being shown.
    //private int currTimeSig = 4;

    // Model for the table, which provides the link to the column objects.
    private JPatternTableModel table = new JPatternTableModel(1, 1);

    /**
     * The JTrackerColumns currently being displayed.
     */
    protected JTrackerColumn[] columns;

    /**
     * The cursor position within the current column, from 0 to
     * columns[gridX].columnWidth-1.
     */
    protected int columnPosition = 0;

    /**
     * The X grid position of the cursor, from 0 to columns.length-1.
     */
    protected int gridX = 0;

    /**
     * The Y grid position of the cursor, from 0 to currPattern.length-1.
     */
    protected int gridY = 0;

    /**
     * The constructor accepts a reference to the system composer.
     * @param composer
     */
    JPatternTable(SystemComposerPatternData composer, JTrackerPanel aThis, JPanel panel)
    {
        sysComposer = composer;
        trackerPanel = aThis;
        backPanel = panel;
        //cursors = curs;

        // Set the table model to our customised class.
        setModel(table);

        // Customise the appearance.
        showHorizontalLines = false;
        showVerticalLines = false;
        setIntercellSpacing(new Dimension(0,0));

        // Customise resizing behaviour.
        setAutoResizeMode(AUTO_RESIZE_OFF);
        getTableHeader().setResizingAllowed(false);

        // Set the cell renderer to our custom class.
        cellRenderer = new JPatternTableCellRenderer(this);
        setDefaultRenderer(Object.class, cellRenderer);

        // Enable mouse selection.
        cellSelectionEnabled = true;

        // Clear the table for first use.
        clearData();

        // Add our custom listener for 'editing' cells.
        addKeyListener(new PatternKeyListener(this));
    }

    /**
     * Intercepts changes to the font of the control and changes the widths
     * of the columns being shown to suit the new font size.
     */
    @Override
    public void setFont(Font f)
    {
        super.setFont(f);
        scaleColumns();
    }

    /**
     * Shows a given pattern. The machine name is also required, for locating
     * the pattern. If there is a pattern showing which needs to be saved, it
     * is saved. 
     */
    public void showPattern(PatternNode patt, MachineNode mach)
    {
        saveCurrentPattern();

        clearData();

        currPatternChanged = false;
        currPattern = patt;
        currMachine = mach;

        if (patt == null) return;
        
        //currTimeSig = patt.timeSig;
        table.setRowCount(currPattern.patternLength);
        setupColumns();

        cellRenderer.clearCache(columns.length, currPattern.patternLength);

        setColumnSelectionInterval(0, 0);
        setRowSelectionInterval(0, 0);

        Dimension s = getPreferredSize();
        s.height += getRowHeight(); // For the header
        Rectangle r = new Rectangle(0, 0, s.width, s.height);
        setBounds(r);
        backPanel.setPreferredSize(s);
    }

    // Reads the tracks that exist for the current pattern and sets up columns
    // to match them.
    private void setupColumns()
    {
        ArrayList<UsefulFileObject> globalTracks = null;

        if (currPattern.patternID.equals(SystemComposerPlayback.MASTER_PATTERN_NAME))
        {
            globalTracks = sysComposer.getPatternTracks(SystemComposerPlayback.MASTER_PATTERN_NAME, SystemComposerPlayback.MASTER_PATTERN_NAME);
            if (globalTracks == null)
            {
                // This could happen if the master pattern wasn't initialised
                // yet, but shouldn't happen normally. TODO: Check this some
                // time since if this did happen there could be a sync problem.
                globalTracks = new ArrayList<UsefulFileObject>();
            }
        }
        else
        {
            globalTracks = sysComposer.getPatternTracks(currMachine.machineName, currPattern.patternID);
            if (globalTracks == null)
            {
                // This shouldn't happen - we create tracks when we create
                // patterns and the getChildren should only return an empty list
                // anyway.
                globalTracks = new ArrayList<UsefulFileObject>();
            }
        }

        ArrayList<JTrackerColumn> tColumns = new ArrayList<JTrackerColumn>();
        tColumns.add(new JTrackerColumnRowNum());

        // TODO: Merge this with same code from PlayingPattern.
        // Place in PatternNode and unpack all data to arrays.

        // Read global tracks.
        int numColumns = globalTracks.size();
        for (int x = 0; x < numColumns; x++)
        {
            PatternTrackNode node = (PatternTrackNode) globalTracks.get(x);
            if (node != null) doColumn(node, tColumns, node.groupNumber);
        }

        columns = new JTrackerColumn[0];
        columns = tColumns.toArray(columns);
        table.columns = columns;
        table.setColumnCount(columns.length);
        table.setColumnIdentifiers(columns);
        scaleColumns();
    }

    private void doColumn(PatternTrackNode node, ArrayList<JTrackerColumn> tColumns, int trackNum) {
        JTrackerColumn column = null;
        if (node.paramType == ParameterBase.PARAMETER_TYPE_PATTERN) {
            // TODO: Cache the types of machines?
            MachineNode mNode = (MachineNode) Main.document.getValue("Machines/" + node.paramName);//sysComposer.findMachine(node.paramName);
            if (mNode != null) column = new JTrackerColumnMachine(node.columnName, node.paramName, currPattern, node, mNode.machineName, trackNum);
        } else if (node.paramType == ParameterBase.PARAMETER_TYPE_SLIDER) {
            column = new JTrackerColumnSlider(node.columnName, node.paramName, currPattern, node, trackNum);
        } else if (node.paramType == ParameterBase.PARAMETER_TYPE_NOTE) {
            column = new JTrackerColumnNote(node.columnName, node.paramName, currPattern, node, trackNum);
        } else if (node.paramType == ParameterBase.PARAMETER_TYPE_COMMAND) {
            column = new JTrackerColumnCommand(node.columnName, node.paramName, currPattern, node, trackNum);
        } else if (node.paramType == ParameterBase.PARAMETER_TYPE_SAMPLE) {
            column = new JTrackerColumnSample(node.columnName, node.paramName, currPattern, node, trackNum);
        }

        if (column != null) {
            tColumns.add(column);
        }
    }

    // Works out the width of the current font and scales each column to fit
    // based on the width of its entries.
    private void scaleColumns()
    {
        if (columns == null) return;

        TableColumnModel model = getColumnModel();
        FontRenderContext frc = new FontRenderContext(null, false, false);
        int fontWidth = (int) (this.getFont().getStringBounds("00000", frc).getWidth()/5.0);
        for (int x = 0; x < columns.length; x++) {
            TableColumn col = model.getColumn(x);
            col.setPreferredWidth((columns[x].columnWidth + 3) * fontWidth);
        }
    }

    // Checks if the pattern being shown hs been modified and if so writes the
    // tracks and commands back to the composition.
    void saveCurrentPattern()
    {
        if (currPattern == null) return;

        // TODO: Work out why this is fired twice on selection and FOUR times
        // on deselection (by changing machine).
        if (currPatternChanged == false) return;

        sysComposer.savePattern(currMachine.machineName, currPattern.patternID);

        currPatternChanged = false;
    }

    /**
     * Empties the table data, without saving.
     */
    public void clearData()
    {
        currMachine = null;
        currPattern = null;
        columns = new JTrackerColumn[0];

        table.setRowCount(0);
        table.setColumnCount(0);
        table.columns = new JTrackerColumn[0];

        gridX = 0;
        gridY = 0;
        columnPosition = 0;
    }

    /**
     * Updates the display *using the system composer's current pattern*. This
     * is used when tracks are added or removed, so a refresh from the composition
     * is necessary.
     */
    public void refreshPattern()
    {
        showPattern(currPattern, currMachine);
    }

    /**
     * We override the paint method so that each time the display is updated
     * we can change the description showing for the current cursor position.
     */
    @Override
    public void paint(Graphics g)
    {
        super.paint(g);
        if (trackerPanel != null && columns != null && gridX < columns.length)
        {
            trackerPanel.setCursorDescription(columns[gridX].getCursorDescription(gridY, columnPosition));
        }
    }
}



/**
 * Listens for key events on the tracker interface and passes them to columns
 * for interpretation. The table itself handles rerendering.
 */
class PatternKeyListener implements KeyListener
{
    private JPatternTable table;

    public PatternKeyListener(JPatternTable t)
    {
        table = t;
    }

    @Override
    public void keyTyped(KeyEvent e) { }

    @Override
    public void keyPressed(KeyEvent e)
    {
        int colNum = table.gridX;
        int rowNum = table.gridY;

        // We let these keys go through to the default
        // handler, which does things like selection.
        if (   e.getKeyCode() == KeyEvent.VK_SHIFT
            || e.getKeyCode() == KeyEvent.VK_DOWN
            || e.getKeyCode() == KeyEvent.VK_PAGE_DOWN
            || e.getKeyCode() == KeyEvent.VK_UP
            || e.getKeyCode() == KeyEvent.VK_PAGE_UP
            || e.getKeyCode() == KeyEvent.VK_LEFT
            || e.getKeyCode() == KeyEvent.VK_RIGHT)
        {
            // We handle left/right ourselves so that the cursor moves within
            // columns.
            if (e.getKeyCode() == KeyEvent.VK_LEFT)
            {
                table.columnPosition--;
                if (table.columnPosition < 0)
                {
                    if (colNum > 0)
                    {
                        table.columnPosition = table.columns[colNum-1].columnWidth-1;
                    }
                    else table.columnPosition = 0;
                }
                else
                {
                    e.consume();
                }

                table.repaint();
            }
            else if (e.getKeyCode() == KeyEvent.VK_RIGHT)
            {
                table.columnPosition++;
                if (table.columnPosition >= table.columns[colNum].columnWidth)
                {
                    if (colNum < table.columns.length-1)
                    {
                        table.columnPosition = 0;
                    }
                    else table.columnPosition--;
                }
                else
                {
                    e.consume();
                }

                table.repaint();
            }
        }
        // All other keypresses are sent to column handlers.
        else
        {
            if (table.columns.length == 0) return;

            // Pass to column - keycode, row, columnPosition. If something
            // happens we do some work moving the cursor etc.
            if (table.columns[colNum].setEntry(rowNum, table.columnPosition, e))
            {
                rowNum++;
                if (rowNum >= table.getRowCount()) rowNum--;
                table.setRowSelectionInterval(rowNum, rowNum);
                table.repaint();
                table.currPatternChanged = true;
            }

            e.consume();
        }

        // Ensures that the viewport scrolls with the cursor.
        table.scrollRectToVisible(table.getCellRect(rowNum, colNum, false));
    }

    @Override
    public void keyReleased(KeyEvent e) { }
}