package org.limewire.mojito.visual;

import java.awt.EventQueue;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Rectangle2D;
import java.lang.reflect.InvocationTargetException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

import org.limewire.mojito.Context;
import org.limewire.mojito.KUID;
import org.limewire.mojito.io.MessageDispatcher.MessageDispatcherEvent;
import org.limewire.mojito.io.MessageDispatcher.MessageDispatcherListener;
import org.limewire.mojito.io.MessageDispatcher.MessageDispatcherEvent.EventType;
import org.limewire.mojito.messages.RequestMessage;
import org.limewire.mojito.messages.DHTMessage.OpCode;

/**
 * Paints different representations of the Distributed Hash Table (DHT). 
 * <code>ArcsVisualizer</code> is a small framework for visually representing 
 * the DHT messages distinguished by color and dash patterns.
 */
public class ArcsVisualizer extends JPanel implements MessageDispatcherListener {

    private static final int SLEEP = 100;
    
    private static final float FONT_SIZE = 24f;
    
    private final Context context;

    private Timer timer;
    
    public static ArcsVisualizer show(final Context context) {
        final ArcsVisualizer arcs = new ArcsVisualizer(context, context.getLocalNodeID());
        
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame(context.getName());
                frame.getContentPane().add(arcs);
                frame.setBounds(20, 30, 640, 640);
                frame.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        arcs.stopArcs();
                    }
                });
                frame.setVisible(true);
            }
        });
        
        arcs.startArcs();
        return arcs;
    }
    
    public void startArcs() {
        timer.start();
        context.getMessageDispatcher().addMessageDispatcherListener(this);
    }
    
    public void stopArcs() {
        timer.stop();
        context.getMessageDispatcher().removeMessageDispatcherListener(this);
    }
    
    private final Object lock = new Object();
    
    private Painter painter;
    
    private int painterIndex = 0;
    
    private final List<Painter> painters = new ArrayList<Painter>();
    
    public ArcsVisualizer(Context context, KUID nodeId) {
        this.context = context;
        addPainter(new SnowMan(nodeId));
        addPainter(new PlasmaLamp(nodeId));
        addPainter(new DartBoard(nodeId));
        
        painter = painters.get(painterIndex);
        
        Runnable runner = new Runnable() {
            @Override
            public void run() {
                timer = new Timer(SLEEP, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        repaint();
                    }
                });
                
                // Make sure the JPanel is focusable or it won't fire 
                // FocusEvent and Component's hasFocus() method returns 
                // always false
                setFocusable(true);
                
                addMouseListener(new MouseAdapter() {
                    
                    private volatile boolean hadFocus = false;
                    
                    @Override
                    public void mousePressed(MouseEvent e) {
                        hadFocus = e.getComponent().isFocusOwner();
                        // Request focus if not focused.  This can occur if the 
                        // window is newly activated.
                        if (!hadFocus) {
                            ArcsVisualizer.this.requestFocusInWindow();
                        }
                    }

                    @Override
                    public void mouseClicked(MouseEvent e) {
                        if (!hadFocus) {
                            return;
                        }
                        
                        synchronized (lock) {
                            painter.clear();
                            
                            painterIndex = (painterIndex + 1) % painters.size();
                            painter = painters.get(painterIndex);
                        }
                    }
                });
            }
        };
        
        if(EventQueue.isDispatchThread()) {
            runner.run();
        } else {
            try {
                EventQueue.invokeAndWait(runner);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    public void addPainter(Painter painter) {
        synchronized (lock) {
            painters.add(painter);
        }
    }
    
    @Override
    public void paint(Graphics g) {
        float width = getWidth();
        float height = getHeight();
        
        Graphics2D g2 = (Graphics2D)g;
        
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_ON);
        
        g2.setColor(Color.black);
        g2.fill(new Rectangle2D.Float(0, 0, width, height));
        
        g2.setFont(getFont().deriveFont(FONT_SIZE));
        g2.setColor(Color.green);
        FontMetrics fm = g2.getFontMetrics();
        g2.drawString("Input", width/4f - fm.stringWidth("Input")/2f, 30);
        g2.setColor(Color.red);
        g2.drawString("Output", width-width/4f - fm.stringWidth("Output")/2f, 30);
        
        g2.setFont(getFont().deriveFont(FONT_SIZE/2));
        fm = g2.getFontMetrics();
        g2.setColor(new Color(0, 255, 0));
        g2.drawString("response", width/4f - fm.stringWidth("response")/2f, 48);
        g2.setColor(new Color(0, 255, 255));
        g2.drawString("request", width/4f - fm.stringWidth("request")/2f, 60);
        
        g2.setColor(new Color(255, 0, 0));
        g2.drawString("request", width-width/4f - fm.stringWidth("request")/2f, 48);
        g2.setColor(new Color(255, 0, 255));
        g2.drawString("response", width-width/4f - fm.stringWidth("response")/2f, 60);
        
        synchronized (lock) {
            painter.paint(this, g2);
        }
    }
    
    /*
     * (non-Javadoc)
     * @see org.limewire.mojito.io.MessageDispatcher.MessageDispatcherListener#handleMessageDispatcherEvent(org.limewire.mojito.io.MessageDispatcher.MessageDispatcherEvent)
     */
    public void handleMessageDispatcherEvent(MessageDispatcherEvent evt) {
        EventType type = evt.getEventType();
        KUID nodeId = null;
        SocketAddress addr = null;
        
        if (type.equals(EventType.MESSAGE_RECEIVED)) {
            nodeId = evt.getMessage().getContact().getNodeID();
            addr = evt.getMessage().getContact().getContactAddress();
        } else if (type.equals(EventType.MESSAGE_SENT)) {
            nodeId = evt.getNodeID();
            addr = evt.getSocketAddress();
        } else {
            return;
        }
        
        OpCode opcode = evt.getMessage().getOpCode();
        boolean request = (evt.getMessage() instanceof RequestMessage);
        synchronized (lock) {
            painter.handle(type, nodeId, addr, opcode, request);
        }
    }
    
    @SuppressWarnings({"InfiniteLoopStatement"})
    public static void main(String[] args) throws Exception {
        ArcsVisualizer arcs = new ArcsVisualizer(null, KUID.createRandomID());
        
        JFrame frame = new JFrame();
        frame.getContentPane().add(arcs);
        frame.setBounds(20, 30, 640, 480);
        frame.setVisible(true);
        
        Random generator = new Random();
        
        EventType type = null;
        KUID nodeId = null;
        
        final int sleep = 100;
        
        while(true) {

            // Simulate an received or sent message
            nodeId = KUID.createRandomID();
            if (generator.nextBoolean()) {
                type = EventType.MESSAGE_RECEIVED;
            } else {
                type = EventType.MESSAGE_SENT;
            }
            
            OpCode opcode = OpCode.valueOf(1 + generator.nextInt(OpCode.values().length-1));
            //OpCode opcode = OpCode.FIND_NODE_REQUEST;
            
            arcs.painter.handle(type, nodeId, null, opcode, true);
            
            // Sleep a bit...
            //Thread.sleep(sleep);
            Thread.sleep(generator.nextInt(sleep));
            
            // Send every now an then a response
            if (generator.nextBoolean()) {
                if (type.equals(EventType.MESSAGE_SENT)) {
                    arcs.painter.handle(EventType.MESSAGE_RECEIVED, nodeId, null, opcode, false);
                } else {
                    arcs.painter.handle(EventType.MESSAGE_SENT, nodeId, null, opcode, false);
                }
            }
        }
    }
}
