/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI 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 at gnu.org.
 */

package com.eviware.soapui.support.log;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.concurrent.Future;

import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

import org.apache.commons.collections.list.TreeList;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.support.UISupport;

/**
 * Component for displaying log entries
 * 
 * @author Ole.Matzura
 */

public class JLogList extends JPanel
{
	private long maxRows = 1000;
	private JList logList;
	private SimpleAttributeSet requestAttributes;
	private SimpleAttributeSet responseAttributes;
	private LogListModel model;
	private List<Logger> loggers = new ArrayList<Logger>();
	private InternalLogAppender internalLogAppender = new InternalLogAppender();
	private boolean tailing = true;
	private Stack<Object> linesToAdd = new Stack<Object>();
	private EnableAction enableAction;
	private JCheckBoxMenuItem enableMenuItem;
	private final String title;
	private boolean released;
	private Future<?> future;

	public JLogList( String title )
	{
		super( new BorderLayout() );
		this.title = title;

		model = new LogListModel();
		logList = new JList( model );
		logList.setToolTipText( title );
		logList.setCellRenderer( new LogAreaCellRenderer() );
		logList.setPrototypeCellValue( "Testing 123" );
		logList.setFixedCellWidth( -1 );

		JPopupMenu listPopup = new JPopupMenu();
		listPopup.add( new ClearAction() );
		enableAction = new EnableAction();
		enableMenuItem = new JCheckBoxMenuItem( enableAction );
		enableMenuItem.setSelected( true );
		listPopup.add( enableMenuItem );
		listPopup.addSeparator();
		listPopup.add( new CopyAction() );
		listPopup.add( new SetMaxRowsAction() );
		listPopup.addSeparator();
		listPopup.add( new ExportToFileAction() );

		logList.setComponentPopupMenu( listPopup );

		setBorder( BorderFactory.createEmptyBorder( 3, 3, 3, 3 ) );
		JScrollPane scrollPane = new JScrollPane( logList );
		UISupport.addPreviewCorner( scrollPane, true );
		add( scrollPane, BorderLayout.CENTER );

		requestAttributes = new SimpleAttributeSet();
		StyleConstants.setForeground( requestAttributes, Color.BLUE );

		responseAttributes = new SimpleAttributeSet();
		StyleConstants.setForeground( responseAttributes, Color.GREEN );

		try
		{
			maxRows = Long.parseLong( SoapUI.getSettings().getString( "JLogList#" + title, "1000" ) );
		}
		catch( NumberFormatException e )
		{
		}
	}

	public void clear()
	{
		model.clear();
	}

	public JList getLogList()
	{
		return logList;
	}

	public long getMaxRows()
	{
		return maxRows;
	}

	public void setMaxRows( long maxRows )
	{
		this.maxRows = maxRows;
	}

	public synchronized void addLine( Object line )
	{
		if( !isEnabled() )
			return;

		synchronized( model.lines )
		{
			if( line instanceof LoggingEvent )
			{
				LoggingEvent ev = ( LoggingEvent )line;
				linesToAdd.push( new LoggingEventWrapper( ev ) );

				if( ev.getThrowableInformation() != null )
				{
					Throwable t = ev.getThrowableInformation().getThrowable();
					StringWriter sw = new StringWriter();
					PrintWriter pw = new PrintWriter( sw );
					t.printStackTrace( pw );
					StringTokenizer st = new StringTokenizer( sw.toString(), "\r\n" );
					while( st.hasMoreElements() )
						linesToAdd.push( "   " + st.nextElement() );
				}
			}
			else
			{
				linesToAdd.push( line );
			}
		}
		if( future == null )
		{
			released = false;
			future = SoapUI.getThreadPool().submit( model );
		}
	}

	public void setEnabled( boolean enabled )
	{
		super.setEnabled( enabled );
		logList.setEnabled( enabled );
		enableMenuItem.setSelected( enabled );
	}

	private static class LogAreaCellRenderer extends DefaultListCellRenderer
	{
		private Map<Level, Color> levelColors = new HashMap<Level, Color>();

		private LogAreaCellRenderer()
		{
			levelColors.put( Level.ERROR, new Color( 192, 0, 0 ) );
			levelColors.put( Level.INFO, new Color( 0, 92, 0 ) );
			levelColors.put( Level.WARN, Color.ORANGE.darker().darker() );
			levelColors.put( Level.DEBUG, new Color( 0, 0, 128 ) );
		}

		public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected,
				boolean cellHasFocus )
		{
			JLabel component = ( JLabel )super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );

			if( value instanceof LoggingEventWrapper )
			{
				LoggingEventWrapper eventWrapper = ( LoggingEventWrapper )value;

				if( levelColors.containsKey( eventWrapper.getLevel() ) )
					component.setForeground( levelColors.get( eventWrapper.getLevel() ) );
			}

			// Limit the length of the tool tip, to prevent long delays.
			String toolTip = component.getText();
			if( toolTip != null && toolTip.length() > 1000 )
				toolTip = toolTip.substring( 0, 1000 );
			component.setToolTipText( toolTip );

			return component;
		}
	}

	private final static class LoggingEventWrapper
	{
		private final LoggingEvent loggingEvent;
		private String str;

		public LoggingEventWrapper( LoggingEvent loggingEvent )
		{
			this.loggingEvent = loggingEvent;
		}

		public Level getLevel()
		{
			return loggingEvent.getLevel();
		}

		public String toString()
		{
			if( str == null )
			{
				StringBuilder builder = new StringBuilder();
				builder.append( new Date( loggingEvent.timeStamp ) );
				builder.append( ':' ).append( loggingEvent.getLevel() ).append( ':' ).append( loggingEvent.getMessage() );
				str = builder.toString();
			}

			return str;
		}
	}

	public void addLogger( String loggerName, boolean addAppender )
	{
		Logger logger = Logger.getLogger( loggerName );
		if( addAppender )
			logger.addAppender( internalLogAppender );

		loggers.add( logger );
	}

	public Logger[] getLoggers()
	{
		return loggers.toArray( new Logger[loggers.size()] );
	}

	public void setLevel( Level level )
	{
		for( Logger logger : loggers )
		{
			logger.setLevel( level );
		}
	}

	public Logger getLogger( String loggerName )
	{
		for( Logger logger : loggers )
		{
			if( logger.getName().equals( loggerName ) )
				return logger;
		}

		return null;
	}

	private class InternalLogAppender extends AppenderSkeleton
	{
		protected void append( LoggingEvent event )
		{
			addLine( event );
		}

		public void close()
		{
		}

		public boolean requiresLayout()
		{
			return false;
		}
	}

	public boolean monitors( String loggerName )
	{
		for( Logger logger : loggers )
		{
			if( loggerName.startsWith( logger.getName() ) )
				return true;
		}

		return false;
	}

	public void removeLogger( String loggerName )
	{
		for( Logger logger : loggers )
		{
			if( loggerName.equals( logger.getName() ) )
			{
				logger.removeAppender( internalLogAppender );
			}
		}
	}

	public boolean isTailing()
	{
		return tailing;
	}

	public void setTailing( boolean tail )
	{
		this.tailing = tail;
	}

	private class ClearAction extends AbstractAction
	{
		public ClearAction()
		{
			super( "Clear" );
		}

		public void actionPerformed( ActionEvent e )
		{
			model.clear();
		}
	}

	private class SetMaxRowsAction extends AbstractAction
	{
		public SetMaxRowsAction()
		{
			super( "Set Max Rows" );
		}

		public void actionPerformed( ActionEvent e )
		{
			String val = UISupport.prompt( "Set maximum number of log rows to keep", "Set Max Rows",
					String.valueOf( maxRows ) );
			if( val != null )
			{
				try
				{
					maxRows = Long.parseLong( val );
					SoapUI.getSettings().setString( "JLogList#" + title, val );
				}
				catch( NumberFormatException e1 )
				{
					UISupport.beep();
				}
			}
		}
	}

	private class ExportToFileAction extends AbstractAction
	{
		public ExportToFileAction()
		{
			super( "Export to File" );
		}

		public void actionPerformed( ActionEvent e )
		{
			if( model.getSize() == 0 )
			{
				UISupport.showErrorMessage( "Log is empty; nothing to export" );
				return;
			}

			File file = UISupport.getFileDialogs().saveAs( JLogList.this, "Save Log [] to File", "*.log", "*.log", null );
			if( file != null )
				saveToFile( file );
		}
	}

	private class CopyAction extends AbstractAction
	{
		public CopyAction()
		{
			super( "Copy to clipboard" );
		}

		public void actionPerformed( ActionEvent e )
		{
			Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

			StringBuffer buf = new StringBuffer();
			int[] selectedIndices = logList.getSelectedIndices();
			if( selectedIndices.length == 0 )
			{
				for( int c = 0; c < logList.getModel().getSize(); c++ )
				{
					buf.append( logList.getModel().getElementAt( c ).toString() );
					buf.append( "\r\n" );
				}
			}
			else
			{
				for( int c = 0; c < selectedIndices.length; c++ )
				{
					buf.append( logList.getModel().getElementAt( selectedIndices[c] ).toString() );
					buf.append( "\r\n" );
				}
			}

			StringSelection selection = new StringSelection( buf.toString() );
			clipboard.setContents( selection, selection );
		}
	}

	private class EnableAction extends AbstractAction
	{
		public EnableAction()
		{
			super( "Enable" );
		}

		public void actionPerformed( ActionEvent e )
		{
			JLogList.this.setEnabled( enableMenuItem.isSelected() );
		}
	}

	/**
	 * Internal list model that for optimized storage and notifications
	 * 
	 * @author Ole.Matzura
	 */

	@SuppressWarnings( "unchecked" )
	private final class LogListModel extends AbstractListModel implements Runnable
	{
		private List<Object> lines = new TreeList();

		public int getSize()
		{
			synchronized( lines )
			{
				return lines.size();
			}
		}

		public Object getElementAt( int index )
		{
			synchronized( lines )
			{
				return lines.get( index );
			}
		}

		public void clear()
		{
			int sz = lines.size();
			if( sz == 0 )
				return;

			synchronized( lines )
			{
				lines.clear();
				fireIntervalRemoved( this, 0, sz - 1 );
			}
		}

		public void run()
		{
			Thread.currentThread().setName( "LogList Updater for " + title );

			try
			{
				while( !released && !linesToAdd.isEmpty() )
				{
					try
					{
						if( !linesToAdd.isEmpty() )
						{
							SwingUtilities.invokeAndWait( new Runnable()
							{
								public void run()
								{
									try
									{
										synchronized( lines )
										{
											while( !linesToAdd.isEmpty() )
											{
												int sz = lines.size();
												lines.addAll( linesToAdd );
												linesToAdd.clear();
												fireIntervalAdded( LogListModel.this, sz, lines.size() - sz );
											}

											int cnt = 0;
											while( lines.size() > maxRows )
											{
												lines.remove( 0 );
												cnt++ ;
											}

											if( cnt > 0 )
												fireIntervalRemoved( LogListModel.this, 0, cnt - 1 );

											if( tailing )
											{
												logList.ensureIndexIsVisible( lines.size() - 1 );
											}
										}
									}
									catch( Throwable e )
									{
										SoapUI.logError( e );
									}
								}
							} );
						}

						Thread.sleep( 500 );
					}
					catch( Throwable e )
					{
						SoapUI.logError( e );
					}
				}
			}
			finally
			{
				future = null;
			}
		}
	}

	public void release()
	{
		released = true;
	}

	public void saveToFile( File file )
	{
		try
		{
			PrintWriter writer = new PrintWriter( file );
			for( int c = 0; c < model.getSize(); c++ )
			{
				writer.println( model.getElementAt( c ) );
			}

			writer.close();
		}
		catch( Exception e )
		{
			UISupport.showErrorMessage( e );
		}
	}
}
