/*
Copyright (C) 2005 David Green <green@couchpotato.net>
All Rights Reserved.

This file is part of Aelfengard.

Aelfengard is proprietary software. You may not redistribute it without
prior written permission from the copyright holder.
*/

package common.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import common.Face;

/**
 * The big black area where text from the game is displayed to clients.
 * 
 * @author David Green <green@couchpotato.net>
 */
public class DisplayPanel extends JPanel {

    private static final long serialVersionUID = 3833181437003642169L;

    // So we can reset it later
    private static final int DEFAULT_DIVIDER_SIZE = new JSplitPane().getDividerSize();
    
    private JTextPane chatArea; // the main display textpane
    private JTextPane bufferArea; // another textpane for when you scroll up
    private JScrollPane chatScroll; // for scrolling around in the chatarea
    private JSplitPane chatSplit; // the split that appears/disappears when you scroll up
    private JScrollPane bufferScroll; // for scrolling around in the buffer area
    private JScrollBar scrollBar; // the scroll bar next to the text panes
    private boolean split; // are we currently scrolled up (split)?
    private Runnable onText; // this is run whenever new text is displayed from the server 
    private static StyledDocument doc; // the styleddocument containing the game output
    private static Style defStyle; // the default style of the output text

    /**
     * Create a new display panel.
     */
    public DisplayPanel() {
        super(new BorderLayout());
        chatArea = makeDisplayPane();
        bufferArea = makeDisplayPane();
        bufferArea.setFocusable(false);
        doc = chatArea.getStyledDocument();
        bufferArea.setStyledDocument(doc); // point textpanes to the same document

        defStyle = chatArea.addStyle("default", null);
        StyleConstants.setForeground(defStyle, Color.lightGray);

        bufferScroll = new JScrollPane(bufferArea);
        chatScroll = new JScrollPane(chatArea);
        scrollBar = chatScroll.getVerticalScrollBar();
        chatSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, chatScroll, null);
        chatSplit.setDividerSize(0);
        
        bufferScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
        chatScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
        
        add(chatSplit, BorderLayout.CENTER);
        add(scrollBar, BorderLayout.EAST);

        bufferArea.addCaretListener(new CaretListener() {
            // Keep auto-scrolled to bottom
            public void caretUpdate(CaretEvent e) {
                if (e.getDot() != doc.getLength()) {
                    bufferArea.setCaretPosition(doc.getLength());
                }
            }
        });
        chatArea.addCaretListener(new CaretListener() {
            public void caretUpdate(CaretEvent e) {
                if (split) {
                    // Don't auto-scroll to bottom
                    if (e.getDot() == doc.getLength() && e.getDot() > 0) {
                        chatArea.setCaretPosition(e.getDot() - 1);
                    }
                } else {
                    // Keep auto-scrolled to bottom
                    if (e.getDot() != doc.getLength()) {
                        bufferArea.setCaretPosition(doc.getLength());
                    }
                }
            }
        });
        scrollBar.addAdjustmentListener(new AdjustmentListener() {
            // I don't recall why we had to check for firstTime-ness of this.
            boolean firstTime = true;

            private int lastValue = -1;

            private int lastMax = -1;

            public void adjustmentValueChanged(AdjustmentEvent e) {
                // Don't split on empty document
                if (doc.getLength() == 0) {
                    return;
                }
                if (!split && chatArea.getCaretPosition() != doc.getLength()) {
                    chatArea.setCaretPosition(doc.getLength());
                }
                if (lastValue == e.getValue() // make sure we've stopped adjusting...
                        && lastMax == e.getAdjustable().getMaximum() // and not because of new output?
                        && !firstTime) {
                    if (e.getAdjustable().getVisibleAmount() + e.getValue() >= e
                            .getAdjustable().getMaximum()) { // if we're scrolled to bottom...
                        split(false);
                    } else { // if we're not scrolled to bottom...
                        split(true);
                    }
                }
                // save these for next time...
                lastValue = e.getValue();
                lastMax = e.getAdjustable().getMaximum();
                firstTime = false;
            }
        });
        // re-scroll to bottom whenever split is adjusted
        chatSplit.addPropertyChangeListener("lastDividerLocation",
                new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent evt) {
                        if (doc.getLength() > 0) {
                            chatArea.setCaretPosition(doc.getLength() - 1);
                        }
                        chatArea.setCaretPosition(doc.getLength());
                    }
                });
    }
    
    /**
     * Adds/removes the display area's split.
     * @param newSplit true if the display area should be split
     */
    private void split(boolean newSplit) {
        if (split == newSplit) {
            return;
        }
        split = newSplit;
        if (newSplit) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    chatSplit.setBottomComponent(bufferScroll);
                    chatSplit.setDividerLocation(0.8);
                    chatSplit.setDividerSize(DEFAULT_DIVIDER_SIZE);
                    chatArea.setCaretPosition(doc.getLength() - 1); // don't  autoscroll
                    bufferArea.setCaretPosition(doc.getLength() - 1); // autoscroll
                    bufferArea.setCaretPosition(doc.getLength()); // autoscroll
                }
            });
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    chatSplit.remove(bufferScroll);
                    chatSplit.setDividerSize(0);
                    chatArea.setCaretPosition(doc.getLength()); // autoscroll
                }
            });
        }
    }

    /**
     * Display the specified Face and text.
     * @param face the face to display
     * @param text the text to display, null for no text
     */
    public void sendFace(Face face, String... text) {
        send(face, null, text);
    }
    
    /**
     * Display the specified image and text. 
     * @param filename the relative path of the image to display
     * @param text the text to display, null for no text
     */
    public void sendImage(String filename, String... text) {
        send(null, filename, text);
    }
    
    /**
     * Sends image OR face; just combined to share some logic.
     * @param face the face to send, or null for no face
     * @param image the relative path of the image to display, or null for no image
     * @param text the text to display, null for no text
     */
    private void send(final Face face, final String image, final String... text) {
        if (!SwingUtilities.isEventDispatchThread()) {
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        send(face, image, text);
                    }
                });
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            return;
        }
        final JLabel imageLabel = new JLabel();
        if (face != null) {
            // placeholder
            imageLabel.setIcon(FaceIcon.LOADING_IMAGE);
        }
        if (text.length == 0) {
            println("");
            chatArea.setCaretPosition(doc.getLength());
            chatArea.insertComponent(imageLabel);
        }
        else {
            JPanel imagePanel = new JPanel(new BorderLayout());
            imagePanel.setOpaque(false);
            JTextPane textPane = makeDisplayPane();
            imagePanel.add(imageLabel, BorderLayout.WEST);
            imagePanel.add(textPane, BorderLayout.CENTER);
            StringBuffer sb = new StringBuffer();
            for (String s : text) {
                if (sb.length() > 0) {
                    sb.append("\n\n");
                }
                sb.append(s);
            }
            println("");
            println(textPane.getStyledDocument(), sb.toString());
            chatArea.setCaretPosition(doc.getLength());
            chatArea.insertComponent(imagePanel);
        }
        println("");
        // Start a new thread to load the actual face or image.
        new Thread() {
            @Override
            public void run() {
                try {
                    final Icon icon;
                    if (image != null) {
                        // image
                        icon = GameClient.getImageIcon("graphics/" + image);
                    }
                    else {
                        // face
                        icon = new FaceIcon(face);
                    }
                    SwingUtilities.invokeLater(new Runnable() {
                    
                        public void run() {
                            imageLabel.setIcon(icon);
                            chatArea.setCaretPosition(doc.getLength() - 1);
                            chatArea.setCaretPosition(doc.getLength());
                        }
                    
                    });
                }
                catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }.start();
    }
    
    /**
     * Display the specified table.
     * @param data the data cells to display
     * @param headers the column headers
     * @param sizes the relative column sizes
     */
    public void printTable(final String[][] data, final String[] headers, final int[] sizes) {
        TableModel myModel = new AbstractTableModel() {
        
            private static final long serialVersionUID = 3256719597939275573L;

            @Override
            public String getColumnName(int column) {
                return headers[column];
            }

            public Object getValueAt(int rowIndex, int columnIndex) {
                return data[rowIndex][columnIndex];
            }
        
            public int getRowCount() {
                return data.length;
            }
        
            public int getColumnCount() {
                return headers.length;
            }
            
        };
        final JTable table = new JTable(myModel);
        final JTextArea area = new JTextArea();
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        area.setForeground(Color.white);
        area.setOpaque(true);
        area.setBorder(new EmptyBorder(0, 1, 0, 1));
        area.setBackground(new Color(15, 15, 80));
        final TableCellRenderer oldRenderer = table.getDefaultRenderer(Object.class);
        table.setDefaultRenderer(Object.class, new TableCellRenderer() {
            
                public Component getTableCellRendererComponent(JTable t, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
                    if (isSelected) {
                        area.setText((String) value);
                        area.setSize(
                                t.getColumnModel().getColumn(column).getWidth(),
                                Integer.MAX_VALUE);
                        int height = area.getPreferredSize().height;
                        if (t.getRowHeight(row) < height) {
                            t.setRowHeight(row, height);
                        }
                        return area;
                    }
                    int height = t.getRowHeight();
                    if (t.getRowHeight(row) != height) {
                        t.setRowHeight(row, height);
                    }
                    return oldRenderer.getTableCellRendererComponent(
                            t, value, isSelected, hasFocus, row, column);
                }
            
            });
        JTableHeader header = table.getTableHeader();
        final JPanel headerPanel = new JPanel(new BorderLayout(5, 5));
        headerPanel.setBackground(Color.black);
        headerPanel.setOpaque(true);
        headerPanel.setBorder(new CompoundBorder(new LineBorder(Color.white, 1), new EmptyBorder(5, 5, 5, 5)));

        final JLabel headerLabel = new JLabel();
        final JLabel arrowLabel = new JLabel("\u21E7");
        Font font = headerLabel.getFont();
        font = font.deriveFont(Font.BOLD, font.getSize2D() + 2.0f);
        headerLabel.setFont(font);
        arrowLabel.setFont(font);
        headerLabel.setForeground(Color.white);
        arrowLabel.setForeground(Color.cyan);
        headerPanel.add(arrowLabel, BorderLayout.WEST);
        headerPanel.add(headerLabel, BorderLayout.CENTER);
        header.setDefaultRenderer(new TableCellRenderer() {
        
            public Component getTableCellRendererComponent(JTable jtable, Object value,
                    boolean isSelected, boolean hasFocus, int row, int column) {
                headerLabel.setText((String) value);
                return headerPanel;
            }
        
        });
        table.setBackground(Color.black);
        table.setForeground(Color.white);
        table.setBorder(new LineBorder(Color.white, 1));
        JPanel p = new JPanel(new BorderLayout());
        p.add(table, BorderLayout.CENTER);
        p.add(table.getTableHeader(), BorderLayout.SOUTH);
        println("");
        chatArea.setCaretPosition(doc.getLength());
        chatArea.insertComponent(p);
        println("");
        table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                int total = 0;
                int remaining = table.getWidth();
                if (sizes != null) {
                    TableColumnModel columnModel = table.getColumnModel();
                    for (int i = 0; i < sizes.length; i++) {
                        if (sizes[i] >= 0) {
                            TableColumn column = table.getColumnModel().getColumn(i);
                            column.setWidth(sizes[i]);
                            column.setMinWidth(sizes[i]);
                            column.setMaxWidth(sizes[i]);
                            column.setPreferredWidth(sizes[i]);
                            remaining -= sizes[i];
                            total++;
                        }
                    }
                    if (remaining < 0) {
                        remaining = 0;
                    }
                    int count = columnModel.getColumnCount();
                    total = count - total; // # of remaining columns
                    remaining /= total; // pixels left per column
                    for (int i = 0; i < count; i++) {
                        if (i >= sizes.length || sizes[i] < 0) {
                            TableColumn column = table.getColumnModel().getColumn(i);
                            column.setWidth(remaining);
                            column.setMinWidth(remaining);
                            column.setMaxWidth(remaining);
                            column.setPreferredWidth(remaining);
                        }
                    }
                }
                int count = table.getColumnModel().getColumnCount();
                for (int i = 0; i < count; i++) {
                    TableColumn column = table.getColumnModel().getColumn(i);
                    column.setMinWidth(0);
                    column.setMaxWidth(Integer.MAX_VALUE);
                }
            }
        });
    }

    /**
     * Print the specified text, followed by a linefeed.
     * @param text the text to display
     */
    public void println(String text) {
        print(doc, text + '\n');
    }
    
    /**
     * Print the specified text, without a linefeed.
     * @param text the text to display
     */
    public void print(String text) {
        print(doc, text);
    }
    
    /**
     * Print the specified text into the specified StyledDocument, 
     * followed by a linefeed.
     * 
     * @param sd the target styled document for the output 
     * @param text the text to display
     */
    private void println(StyledDocument sd, String text) {
        print(sd, text + '\n');
    }

    /**
     * Print the specified text into the specified StyledDocument, 
     * without a linefeed.
     * 
     * @param sd the target styled document for the output 
     * @param text the text to display
     */
    private void print(final StyledDocument sd, final String text) {
        if (!SwingUtilities.isEventDispatchThread()) {
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        print(sd, text);
                    }
                });
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            return;
        }
        // @007
        int defaultAttributes = 0;
        int defaultBackground = 0;
        int defaultForeground = 7;
        try {
            Style style = defStyle; // start with default style in case no other
            // style specified
            char[] c = text.toCharArray();
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < c.length; i++) {
                if (c[i] == '@' && i <= c.length - 4) {
                    // Check for a 3-digit code
                    try {
                        Style newStyle = sd.addStyle(null, null);
                        int foreground = parseColor(c[i + 3], defaultForeground);
                        int background = parseColor(c[i + 2], defaultBackground);
                        int attributes = parseColor(c[i + 1], defaultAttributes);

                        Color foregroundColor = null;
                        Color backgroundColor = null;

                        // Let's find hard-to-read combinations and change them to better alternatives:
                        switch (background * 16 + foreground) {
                        case 0x00:
                            foreground = 7;
                            break;
                        case 0x11:
                            foreground = 9;
                            break;
                        case 0x22:
                            foreground = 10;
                            break;
                        case 0x33:
                            foreground = 11;
                            break;
                        case 0x44:
                            foreground = 12;
                            backgroundColor = new Color(0, 0, 32);
                            break;
                        case 0x55:
                            foreground = 13;
                            break;
                        case 0x66:
                            foreground = 14;
                            break;
                        case 0x77:
                            foreground = 15;
                            break;
                        case 0x88:
                            foreground = 0;
                            break;
                        case 0x99:
                            foreground = 1;
                            break;
                        case 0xAA:
                            foreground = 2;
                            break;
                        case 0xBB:
                            foreground = 3;
                            break;
                        case 0xCC:
                            foregroundColor = new Color(0, 0, 32);
                            break;
                        case 0xDD:
                            foreground = 5;
                            break;
                        case 0xEE:
                            foreground = 6;
                            break;
                        case 0xFF:
                            foreground = 8;
                            break;
                        case 0x38:
                            foreground = 7;
                            break;
                        case 0x83:
                            foreground = 11;
                            break;
                        case 0xAE:
                            foreground = 6;
                            break;
                        case 0xEA:
                            foreground = 2;
                            break;
                        case 0xBF:
                            foreground = 8;
                            break;
                        case 0xFB:
                            foreground = 3;
                            break;
                        case 0xAB:
                            foreground = 3;
                            break;
                        case 0xBA:
                            foreground = 2;
                            break;
                        }

                        if (foregroundColor == null) {
                            foregroundColor = colorForNumber(foreground);
                        }
                        if (backgroundColor == null) {
                            backgroundColor = colorForNumber(background);
                        }
                        StyleConstants.setForeground(newStyle, foregroundColor);
                        StyleConstants.setBackground(newStyle, backgroundColor);

                        boolean bold = attributes % 2 == 1;
                        StyleConstants.setBold(newStyle, bold);

                        boolean italic = (attributes >> 1) % 2 == 1;
                        StyleConstants.setItalic(newStyle, italic);

                        boolean underline = (attributes >> 2) % 2 == 1;
                        StyleConstants.setUnderline(newStyle, underline);

                        // boolean strikethrough = (attributes >> 3) % 2 == 1;
                        // StyleConstants.setStrikeThrough(newStyle,
                        // strikethrough); // disabled

                        if (i == 0) { // if found at line start, make this the
                            // default for the line
                            defaultForeground = foreground;
                            defaultBackground = background;
                            defaultAttributes = attributes;
                        }
                        // /////////////////
                        i += 3; // Remove it from the result
                        if (sb.length() > 0) {
                            sd.insertString(sd.getLength(), sb.toString(),
                                    style);
                            sb = new StringBuffer();
                        }
                        style = newStyle;
                        continue;
                    } catch (NumberFormatException ex) {
                        // Guess it's not a valid value...
                    }
                }
                sb.append(c[i]);
            }
            sd.insertString(sd.getLength(), sb.toString(), style);
            if (onText != null) {
                onText.run();
            }
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
    }

    private Color colorForNumber(int n) {
        switch (n % 16) {
        case 1:
            return new Color(128, 0, 0); // dark red
        case 2:
            return new Color(0, 179, 0); // dark green
        case 3:
            return new Color(128, 128, 0); // dark yellow
        case 4:
            return new Color(0, 0, 128); // dark blue
        case 5:
            return new Color(128, 0, 128); // dark magenta
        case 6:
            return new Color(0, 128, 128); // dark cyan
        case 7:
            return new Color(192, 192, 192); // light gray
        case 8:
            return new Color(128, 128, 128); // dark gray
        case 9:
            return new Color(255, 0, 0); // bright red
        case 10:
            return new Color(0, 255, 0); // bright green
        case 11:
            return new Color(255, 255, 0); // bright yellow
        case 12:
            return new Color(64, 64, 255); // bright blue
        case 13:
            return new Color(255, 0, 255); // bright magenta
        case 14:
            return new Color(0, 255, 255); // bright cyan
        case 15:
            return new Color(255, 255, 255); // bright white
        default:
            return new Color(0, 0, 0); // black
        }
    }

    private JTextPane makeDisplayPane() {
        JTextPane ret = new JTextPane();
        ret.setBackground(Color.black);
        ret.setFont(new Font("Monospaced", Font.PLAIN, 14));
        ret.setEditable(false);
        return ret;
    }

    /**
     * Clears the display area.
     */
    public void clear() {
        chatArea.setText("");
    }
    
    private int parseColor(char c, int def) {
        if (c == 'Z' || c == 'z') {
            return def;
        } else {
            return Integer.parseInt(String.valueOf(c), 16);
        }
    }
    
    /**
     * Sets the runnable to run when new text appears from the server.
     * @param r the runnable to run
     */
    public void setOnText(Runnable r) {
        this.onText = r;
    }

}