/*
 * BufferSet.java - An ordered list of buffers.
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2008, 2010 Matthieu Casanova
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package org.gjt.sp.jedit.bufferset;

//{{{ Imports
import org.gjt.sp.jedit.Buffer;
import org.gjt.sp.jedit.jEdit;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.StandardUtilities;

import javax.swing.event.EventListenerList;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import javax.annotation.Nullable;
//}}}

/** An ordered list of buffers, normally attached to an EditPane
 *  and viewed in a BufferSwitcher.
 *
 * @author Matthieu Casanova
 * @since jEdit 4.3pre15
 */
public class BufferSet
{
	/** Possible scopes for BufferSets. */
	public enum Scope { editpane, view, global }

	//{{{ BufferSet constructor
	public BufferSet(BufferSet source)
	{
		if (source == null)
			buffers = Collections.synchronizedList(new ArrayList<>());
		else
			buffers = Collections.synchronizedList(new ArrayList<>(source.buffers));
		listeners = new EventListenerList();

		if (jEdit.getBooleanProperty("sortBuffers"))
		{
			if (jEdit.getBooleanProperty("sortByName"))
				sorter = nameSorter;
			else
				sorter = pathSorter;
		}
	}//}}}

	//{{{ addBuffer() method
	/**
	 * Internal use only, use
	 * {@link org.gjt.sp.jedit.bufferset.BufferSetManager#addBuffer(org.gjt.sp.jedit.View, org.gjt.sp.jedit.Buffer)}
	 * or
	 * {@link org.gjt.sp.jedit.bufferset.BufferSetManager#addBuffer(org.gjt.sp.jedit.EditPane, org.gjt.sp.jedit.Buffer)}
	 * @param buffer the buffer to be added
	 */
	public void addBuffer(Buffer buffer)
	{
		addBufferAt(buffer,  -1);
	} //}}}

	//{{{ addBufferAt() method
	/**
	 * Internal use only, use
	 * {@link org.gjt.sp.jedit.bufferset.BufferSetManager#addBuffer(org.gjt.sp.jedit.View, org.gjt.sp.jedit.Buffer)}
	 * or
	 * {@link org.gjt.sp.jedit.bufferset.BufferSetManager#addBuffer(org.gjt.sp.jedit.EditPane, org.gjt.sp.jedit.Buffer)}
	 * @param buffer the buffer to be added
	 * @param position the position where it must be added or -1 if we don't care
	 */
	public void addBufferAt(Buffer buffer, int position)
	{
		Log.log(Log.DEBUG, this, hashCode() + " addBufferAt("+buffer+','+position+')');

		Buffer untitledBuffer = null;
		synchronized (buffers)
		{
			if (buffers.size() == 1)
			{
				Buffer buf = buffers.get(0);
				if (buf.isUntitled() && !buf.isDirty())
				{
					untitledBuffer = buf;
				}
			}

			if (sorter != null)
			{
				if (buffers.contains(buffer))
					return;
				buffers.add(buffer);
				buffers.sort(sorter);
				position = buffers.indexOf(buffer);
			}
			else
			{
				int oldPos = buffers.indexOf(buffer);
				if (oldPos != -1)
				{
					if (position == -1)
					{
						return;
					}
					moveBuffer(oldPos, position);
					return;
				}
				int size = buffers.size();
				if (position == -1 || position > size)
				{
					position = size;
				}
				buffers.add(position, buffer);
			}
		}
		BufferSetListener[] listeners = this.listeners.getListeners(BufferSetListener.class);
		Log.log(Log.DEBUG, this, hashCode() + ": Buffer added " + buffer + " at " + position);
		for (BufferSetListener listener : listeners)
		{
			listener.bufferAdded(buffer, position);
		}

		// I don't like this reverse control
		if (untitledBuffer != null)
		{
			jEdit.getBufferSetManager().removeBuffer(this, untitledBuffer);
		}
	} //}}}

	//{{{ getBuffer() method
	/**
	 * Returns the Buffer at the given index.
	 * @param index the index. The index must exists
	 * @return the buffer at the index.
	 */
	public Buffer getBuffer(int index)
	{
		return buffers.get(index);
	} //}}}

	//{{{ getSorter()
	/** @return the Comparator used for this bufferset
	    can be null if there is no sorting 
		@since jEdit 5.2
	*/
	@Nullable
	public Comparator<Buffer> getSorter() {
		return sorter;
	}//}}}
	
	//{{{ getPreviousBuffer() method
	public Buffer getPreviousBuffer(int index)
	{
		if (buffers.isEmpty())
			return null;
		if (buffers.size() < 2)
			return buffers.get(0);
		if (index <= 0)
			return buffers.get(buffers.size() - 1);
		return buffers.get(index - 1);
	} //}}}

	//{{{ getNextBuffer() method
	public Buffer getNextBuffer(int index)
	{
		if (buffers.isEmpty())
			return null;
		if (buffers.size() == 1)
			return buffers.get(0);
		if (index >= buffers.size() - 1)
			return buffers.get(0);
		return buffers.get(index + 1);
	} //}}}

	//{{{ indexOf() method
	public int indexOf(Buffer buffer)
	{
		return buffers.indexOf(buffer);
	} //}}}

	//{{{ contains() method
	/**
	 * Check if the BufferSet contains the given buffer
	 * @param buffer the buffer to search
	 * @return true if it contains this buffer
	 * @since jEdit 5.7pre1
	 */
	public boolean contains(Buffer buffer)
	{
		return buffers.contains(buffer);
	} //}}}

	//{{{ size() method
	public int size()
	{
		return buffers.size();
	} //}}}

	//{{{ getAllBuffers() methods
	public void getAllBuffers(BufferSetListener listener)
	{
		synchronized (buffers)
		{
			for (int i = 0;i<buffers.size();i++)
			{
				Buffer buffer = buffers.get(i);
				Log.log(Log.DEBUG, this, hashCode() + ": Buffer added " + buffer + " at " + i);
				listener.bufferAdded(buffer, i);
			}
		}
	}

	/**
	 * Returns an array of all buffers in this bufferSet.
	 *
	 * @return an array of all Buffers
	 */
	public Buffer[] getAllBuffers()
	{
		Buffer[] buffers = new Buffer[this.buffers.size()];
		return this.buffers.toArray(buffers);
	} //}}}

	//{{{ addBufferSetListener() method
	/**
	 * Add a BufferSetListener.
	 * @param listener the new BufferSetListener
	 */
	public void addBufferSetListener(BufferSetListener listener)
	{
		Log.log(Log.DEBUG, this, hashCode() + ": addBufferSetListener " + listener);
		listeners.add(BufferSetListener.class, listener);
	} //}}}

	//{{{ removeBufferSetListener() method
	/**
	 * Remove a BufferSetListener.
	 * If there are no listeners anymore, remove all buffers from the bufferSet.
	 * @param listener the removed BufferSetListener
	 */
	public void removeBufferSetListener(BufferSetListener listener)
	{
		Log.log(Log.DEBUG, this, hashCode() + ": removeBufferSetListener " + listener);
		listeners.remove(BufferSetListener.class, listener);
	} //}}}

	//{{{ toString() method
	@Override
	public String toString()
	{
		return "BufferSet[nbBuffers="+size()+']';
	} //}}}

	//{{{ sort() method
	/**
	 * Sort the bufferSet (useful if a buffer has been renamed for example
	 * @since jEdit 4.4pre1
	 */
	public void sort()
	{
		if (sorter == null)
			return;
		// do the sort
		buffers.sort(sorter);

		// notify the listeners so they can repaint themselves
		BufferSetListener[] listeners = this.listeners.getListeners(BufferSetListener.class);
		for (BufferSetListener listener : listeners)
		{
			listener.bufferSetSorted();
		}
	} //}}}

	//{{{ Package-private members

	//{{{ handleMessage
	/**
	 * This method is called by BufferSetManager to signal that this
	 * BufferSet needs to react to a change in the sorting properties.
	 */
	void propertiesChanged()
	{
		if (jEdit.getBooleanProperty("sortBuffers"))
		{
			// set the appropriate sorter
			if (jEdit.getBooleanProperty("sortByName"))
				sorter = nameSorter;
			else
				sorter = pathSorter;

			sort();
		}
		else
		{
			// user has elected not to sort BufferSets
			sorter = null;
		}
	} //}}}

	//{{{ moveBuffer() method
	void moveBuffer(int oldPosition, int newPosition)
	{
		if (sorter != null)
		{
			// Buffers are sorted, do nothing
			return;
		}
		Buffer buffer;
		synchronized (buffers)
		{
			buffer = buffers.remove(oldPosition);
			int size = buffers.size();
			if (newPosition == -1 || newPosition > size)
			{
				newPosition = size;
			}
			buffers.add(newPosition, buffer);
		}
		BufferSetListener[] listeners = this.listeners.getListeners(BufferSetListener.class);
		Log.log(Log.DEBUG, this, hashCode() + ": Buffer moved " + buffer + " from " + oldPosition + " to " + newPosition);
		for (BufferSetListener listener : listeners)
		{
			listener.bufferMoved(buffer, oldPosition, newPosition);
		}
	} //}}}

	//{{{ removeBuffer() method
	void removeBuffer(Buffer buffer)
	{
		int index;
		synchronized (buffers)
		{
			index = buffers.indexOf(buffer);
			if (index == -1)
				return;

			buffers.remove(index);
		}
		BufferSetListener[] listeners = this.listeners.getListeners(BufferSetListener.class);
		Log.log(Log.DEBUG, this, hashCode() + ": Buffer removed " + buffer);
		for (BufferSetListener listener : listeners)
		{
			listener.bufferRemoved(buffer, index);
		}
	} //}}}

	//}}}

	//{{{ Private members
	private final List<Buffer> buffers;
	private final EventListenerList listeners;
	private static final Comparator<Buffer> nameSorter = new NameSorter();
	private static final Comparator<Buffer> pathSorter = new PathSorter();
	private Comparator<Buffer> sorter;
	//}}}

	//{{{ NameSorter class
	public static class NameSorter implements Comparator<Buffer>
	{
		@Override
		public int compare(Buffer o1, Buffer o2)
		{
			int ret = StandardUtilities.compareStrings(o1.getName(), o2.getName(), true);
			if (ret == 0)
			{
				ret = StandardUtilities.compareStrings(o1.getPath(), o2.getPath(), true);
			}
			return ret;
		}
	} //}}}

	//{{{ PathSorter class
	public static class PathSorter implements Comparator<Buffer>
	{
		@Override
		public int compare(Buffer o1, Buffer o2)
		{
			return StandardUtilities.compareStrings(o1.getPath(), o2.getPath(), true);
		}
	} //}}}
}
