/*
 * BufferHistory.java - Remembers caret positions
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2000, 2005 Slava Pestov
 *
 * 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;

//{{{ Imports
import java.io.Closeable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

import org.gjt.sp.jedit.msg.DynamicMenuChanged;
import org.gjt.sp.jedit.textarea.*;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.XMLUtilities;
import org.gjt.sp.util.IOUtilities;
//}}}

/**
 * Recent file list.
 * @author Slava Pestov
 * @version $Id: BufferHistory.java 24428 2016-06-23 02:49:29Z daleanson $
 */
public class BufferHistory
{
	//{{{ Entry class
	/**
	 * Recent file list entry.
	 */
	public static class Entry
	{
		public String path;
		public int caret;
		public String selection;
		public String encoding;
		public String mode;

		public Selection[] getSelection()
		{
			return stringToSelection(selection);
		}

		public Entry(String path, int caret, String selection, String encoding, String mode)
		{
			this.path = path;
			this.caret = caret;
			this.selection = selection;
			this.encoding = encoding;
			this.mode = mode;
		}

		public String toString()
		{
			return path + ": " + caret;
		}
	} //}}}

	//{{{ getEntry() method
	public static Entry getEntry(String path)
	{
		historyLock.readLock().lock();
		try
		{
			for (Entry entry : history)
			{
				if(MiscUtilities.pathsEqual(entry.path,path))
					return entry;
			}
		}
		finally
		{
			historyLock.readLock().unlock();
		}

		return null;
	} //}}}

	//{{{ setEntry() method
	public static void setEntry(String path, int caret, Selection[] selection,
		String encoding, String mode)
	{
		Entry entry = new Entry(path,caret,
			selectionToString(selection), encoding, mode);
		historyLock.writeLock().lock();
		try
		{
			removeEntry(path);
			addEntry(entry);
		}
		finally
		{
			historyLock.writeLock().unlock();
		}
		notifyChange();
	} //}}}

	//{{{ clear() method
	/**
	 * Clear the BufferHistory.
	 * @since 4.3pre6
	 */
	public static void clear()
	{
		historyLock.writeLock().lock();
		try
		{
			history.clear();
		}
		finally
		{
			historyLock.writeLock().unlock();
		}
		notifyChange();
	} //}}}

	//{{{ getHistory() method
	/**
	 * Returns the Buffer list.
	 * @return the buffer history list
	 * @since jEdit 4.2pre2
	 */
	@SuppressWarnings({"unchecked"}) 
	public static List<Entry> getHistory()
	{
		// Returns a snapshot to avoid concurrent access to the
		// history. This requires O(n) time, but it should be ok
		// because this method should be used only by external
		// O(n) operation.

		historyLock.readLock().lock();
		try
		{
			return (List<Entry>)history.clone();
		}
		finally
		{
			historyLock.readLock().unlock();
		}
	} //}}}

	//{{{ load() method
	public static void load()
	{
		if(recentXML == null)
			return;

		if(!recentXML.fileExists())
			return;

		Log.log(Log.MESSAGE,BufferHistory.class,"Loading " + recentXML);

		RecentHandler handler = new RecentHandler();
		try
		{
			recentXML.load(handler);
		}
		catch(IOException e)
		{
			Log.log(Log.ERROR,BufferHistory.class,e);
		}
		trimToLimit(handler.result);
		history = handler.result;
	} //}}}

	//{{{ save() method
	public static void save()
	{
		if(recentXML == null)
			return;

		if(recentXML.hasChangedOnDisk())
		{
			Log.log(Log.WARNING,BufferHistory.class,recentXML
				+ " changed on disk; will not save recent"
				+ " files");
			return;
		}

		Log.log(Log.MESSAGE,BufferHistory.class,"Saving " + recentXML);

		String lineSep = System.getProperty("line.separator");

		SettingsXML.Saver out = null;

		try
		{
			out = recentXML.openSaver();
			out.writeXMLDeclaration();

			out.write("<!DOCTYPE RECENT SYSTEM \"recent.dtd\">");
			out.write(lineSep);
			out.write("<RECENT>");
			out.write(lineSep);

			// Make a snapshot to avoid long locking period
			// which may be required by file I/O.
			List<Entry> snapshot = getHistory();

			for (Entry entry : snapshot)
			{
				out.write("<ENTRY>");
				out.write(lineSep);

				out.write("<PATH>");
				out.write(XMLUtilities.charsToEntities(entry.path,false));
				out.write("</PATH>");
				out.write(lineSep);

				out.write("<CARET>");
				out.write(String.valueOf(entry.caret));
				out.write("</CARET>");
				out.write(lineSep);

				if(entry.selection != null
					&& entry.selection.length() > 0)
				{
					out.write("<SELECTION>");
					out.write(entry.selection);
					out.write("</SELECTION>");
					out.write(lineSep);
				}

				if(entry.encoding != null)
				{
					out.write("<ENCODING>");
					out.write(entry.encoding);
					out.write("</ENCODING>");
					out.write(lineSep);
				}

				if (entry.mode != null)
				{
					out.write("<MODE>");
					out.write(entry.mode);
					out.write("</MODE>");
					out.write(lineSep);
				}

				out.write("</ENTRY>");
				out.write(lineSep);
			}

			out.write("</RECENT>");
			out.write(lineSep);

			out.finish();
		}
		catch(Exception e)
		{
			Log.log(Log.ERROR,BufferHistory.class,e);
		}
		finally
		{
			IOUtilities.closeQuietly((Closeable)out);
		}
	} //}}}

	//{{{ Private members
	private static LinkedList<Entry> history;
	private static final ReentrantReadWriteLock historyLock;
	private static SettingsXML recentXML;

	//{{{ Class initializer
	static
	{
		history = new LinkedList<Entry>();
		historyLock = new ReentrantReadWriteLock();
		String settingsDirectory = jEdit.getSettingsDirectory();
		if(settingsDirectory != null)
		{
			recentXML = new SettingsXML(settingsDirectory, "recent");
		}
	} //}}}

	//{{{ addEntry() method
	private static void addEntry(Entry entry)
	{
		historyLock.writeLock().lock();
		try
		{
			history.addFirst(entry);
			trimToLimit(history);
		}
		finally
		{
			historyLock.writeLock().unlock();
		}
	} //}}}

	//{{{ removeEntry() method
	private static void removeEntry(String path)
	{
		historyLock.writeLock().lock();
		try
		{
			Iterator<Entry> iter = history.iterator();
			while(iter.hasNext())
			{
				Entry entry = iter.next();
				if(MiscUtilities.pathsEqual(path,entry.path))
				{
					iter.remove();
					return;
				}
			}
		}
		finally
		{
			historyLock.writeLock().unlock();
		}
	} //}}}

	//{{{ selectionToString() method
	private static String selectionToString(Selection[] s)
	{
		if(s == null)
			return null;

		StringBuilder buf = new StringBuilder();

		for(int i = 0; i < s.length; i++)
		{
			if(i != 0)
				buf.append(' ');

			Selection sel = s[i];
			if(sel instanceof Selection.Range)
				buf.append("range ");
			else //if(sel instanceof Selection.Rect)
				buf.append("rect ");
			buf.append(sel.getStart());
			buf.append(' ');
			buf.append(sel.getEnd());
		}

		return buf.toString();
	} //}}}

	//{{{ stringToSelection() method
	private static Selection[] stringToSelection(String s)
	{
		if(s == null)
			return null;

		List<Selection> selection = new ArrayList<Selection>();
		StringTokenizer st = new StringTokenizer(s);

		while(st.hasMoreTokens())
		{
			String type = st.nextToken();
			int start = Integer.parseInt(st.nextToken());
			int end = Integer.parseInt(st.nextToken());
			if(end < start)
			{
				// I'm not sure when this can happen,
				// but it does sometimes, witness the
				// jEdit bug tracker.
				continue;
			}

			Selection sel;
			if("range".equals(type))
				sel = new Selection.Range(start,end);
			else //if(type.equals("rect"))
				sel = new Selection.Rect(start,end);

			selection.add(sel);
		}

		Selection[] returnValue = new Selection[selection.size()];
		returnValue = selection.toArray(returnValue);
		return returnValue;
	} //}}}

	//{{{ trimToLimit() method
	private static void trimToLimit(Deque<Entry> list)
	{
		int max = jEdit.getIntegerProperty("recentFiles", 40);
		while(list.size() > max)
			list.removeLast();
	} //}}}

	//{{{ notifyChange() method
	private static void notifyChange()
	{
		EditBus.send(new DynamicMenuChanged("recent-files"));
	} //}}}

	//{{{ RecentHandler class
	private static class RecentHandler extends DefaultHandler
	{
		public LinkedList<Entry> result = new LinkedList<Entry>();

		@Override
		public InputSource resolveEntity(String publicId, String systemId)
		{
			return XMLUtilities.findEntity(systemId, "recent.dtd", getClass());
		}

		@Override
		public void endElement(String uri, String localName, String name)
		{
			if("ENTRY".equals(name))
			{
				result.addLast(new Entry(
					path,caret,selection,
					encoding,
					mode));
				path = null;
				caret = 0;
				selection = null;
				encoding = null;
				mode = null;
			}
			else if("PATH".equals(name))
				path = charData.toString();
			else if("CARET".equals(name))
			{
				try
				{
					String s = charData.toString().trim();

					if (s.length() != charData.length())
					{
						Log.log(Log.WARNING, this,
							"The caret position in recent.xml was wrong: '"+
							charData + "', fixing it");
					}
					caret = Integer.parseInt(s);
				}
				catch (NumberFormatException e)
				{
					Log.log(Log.ERROR, this, "Unable to parse caret position " +
						charData);
				}
			}
			else if("SELECTION".equals(name))
				selection = charData.toString();
			else if("ENCODING".equals(name))
				encoding = charData.toString();
			else if("MODE".equals(name))
				mode = charData.toString();
			charData.setLength(0);
		}

		@Override
		public void characters(char[] ch, int start, int length)
		{
			charData.append(ch,start,length);
		}

		// end HandlerBase implementation

		// private members
		private String path;
		private int caret;
		private String selection;
		private String encoding;
		private String mode;
		private final StringBuilder charData = new StringBuilder();	// NOPMD
	} //}}}

	//}}}
}
