/*
 * Registers.java - Register manager
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2003 Slava Pestov
 * Portions Copyright (C) 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;

//{{{ Imports
import java.awt.datatransfer.*;
import java.awt.Toolkit;
import java.io.*;
import java.util.Arrays;

import org.gjt.sp.jedit.buffer.JEditBuffer;
import org.gjt.sp.jedit.datatransfer.JEditDataFlavor;
import org.gjt.sp.jedit.datatransfer.JEditRichText;
import org.gjt.sp.jedit.datatransfer.TransferHandler;
import org.gjt.sp.jedit.gui.HistoryModel;
import org.gjt.sp.jedit.textarea.TextArea;
import org.gjt.sp.jedit.textarea.Selection;
import org.gjt.sp.util.Log;

import javax.annotation.Nullable;
//}}}

/**
 * jEdit's registers are an extension of the clipboard metaphor.<p>
 *
 * A {@link Registers.Register} is string of text indexed by a
 * single character. Typically the text is taken from selected buffer text
 * and the index character is a keyboard character selected by the user.<p>
 *
 * This class defines a number of static methods
 * that give each register the properties of a virtual clipboard.<p>
 *
 * Two classes implement the {@link Registers.Register} interface. A
 * {@link Registers.ClipboardRegister} is tied to the contents of the
 * system clipboard. jEdit assigns a
 * {@link Registers.ClipboardRegister} to the register indexed under
 * the character <code>$</code>. A
 * {@link Registers.DefaultRegister} is created for registers assigned
 * by the user. In addition, jEdit assigns <code>%</code> to
 * the last text segment selected in the text area. On Windows this is a
 * {@link Registers.DefaultRegister}, on Unix under Java 2 version 1.4, a
 * {@link Registers.ClipboardRegister}.
 *
 * @author Slava Pestov
 * @author John Gellene (API documentation)
 * @version $Id: Registers.java 25698 2023-11-17 01:31:50Z vampire0 $
 */
public class Registers
{
	//{{{ copy() method
	/**
	 * Copies the text selected in the text area into the specified register.
	 * This will replace the existing contents of the designated register.
	 *
	 * @param textArea The text area
	 * @param register The register
	 * @since jEdit 2.7pre2
	 */
	public static void copy(TextArea textArea, char register)
	{
		String selection = textArea.getSelectedText();
		if(selection == null)
			return;

		Transferable transferable = TransferHandler.getInstance().getTransferable(textArea, selection);
		setRegister(register, transferable);
		HistoryModel.getModel("clipboard").addItem(selection);

	} //}}}

	//{{{ cut() method
	/**
	 * Copies the text selected in the text area into the specified
	 * register, and then removes it from the buffer.
	 *
	 * @param textArea The text area
	 * @param register The register
	 * @since jEdit 2.7pre2
	 */
	public static void cut(TextArea textArea, char register)
	{
		if(textArea.isEditable())
		{
			String selection = textArea.getSelectedText();
			if(selection == null)
				return;

			Transferable transferable = TransferHandler.getInstance().getTransferable(textArea, selection);
			setRegister(register,transferable);
			HistoryModel.getModel("clipboard").addItem(selection);

			textArea.setSelectedText("");
		}
		else
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
	} //}}}

	//{{{ append() methods
	/**
	 * Appends the text selected in the text area to the specified register,
	 * with a newline between the old and new text.
	 * @param textArea The text area
	 * @param register The register
	 */
	public static void append(TextArea textArea, char register)
	{
		append(textArea,register,"\n",false);
	}

	/**
	 * Appends the text selected in the text area to the specified register.
	 * @param textArea The text area
	 * @param register The register
	 * @param separator The separator to insert between the old and new text
	 */
	public static void append(TextArea textArea, char register,
		String separator)
	{
		append(textArea,register,separator,false);
	}

	/**
	 * Appends the text selected in the  text area to the specified register.
	 * @param textArea The text area
	 * @param register The register
	 * @param separator The text to insert between the old and new text
	 * @param cut Should the current selection be removed?
	 * @since jEdit 3.2pre1
	 */
	public static void append(TextArea textArea, char register,
		String separator, boolean cut)
	{
		if(cut && !textArea.isEditable())
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}

		String selection = textArea.getSelectedText();
		if(selection == null)
			return;

		Register reg = getRegister(register);

		if(reg != null)
		{
			Transferable transferable = reg.getTransferable();
			if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
			{
				try
				{
					String registerContents = (String) transferable.getTransferData(DataFlavor.stringFlavor);
					if(registerContents.endsWith(separator))
						selection = registerContents + selection;
					else
						selection = registerContents + separator + selection;
				}
				catch (UnsupportedFlavorException e)
				{
				}
				catch (IOException e)
				{
					Log.log(Log.ERROR, Registers.class, e);
				}
			}
		}
		Transferable transferable = TransferHandler.getInstance().getTransferable(textArea, selection);
		setRegister(register,transferable);
		HistoryModel.getModel("clipboard").addItem(selection);

		if(cut)
			textArea.setSelectedText("");
	} //}}}

	//{{{ paste() methods
	/**
	 * Insets the contents of the specified register into the text area.
	 * @param textArea The text area
	 * @param register The register
	 * @since jEdit 2.7pre2
	 */
	public static void paste(TextArea textArea, char register)
	{
		paste(textArea,register,false);
	}

	/**
	 * Insets the contents of the specified register into the text area.
	 * @param textArea The text area
	 * @param register The register
	 * @param preferredDataFlavor the preferred dataflavor. If not available
	 * {@code DataFlavor.stringFlavor} will be used
	 * @since jEdit 4.4pre1
	 */
	public static void paste(TextArea textArea, char register, DataFlavor preferredDataFlavor)
	{
		paste(textArea,register,false, preferredDataFlavor);
	}

	/**
	 * Inserts the contents of the specified register into the text area.
	 * @param textArea The text area
	 * @param register The register
	 * @param vertical Vertical (columnar) paste
	 * @since jEdit 4.1pre1
	 */
	public static void paste(TextArea textArea, char register,
		boolean vertical)
	{
		if(!textArea.isEditable())
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}

		Register reg = getRegister(register);

		if(reg == null)
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		Transferable transferable = reg.getTransferable();
		Mode mode = null;
		String selection = null;
		if (transferable.isDataFlavorSupported(JEditDataFlavor.jEditRichTextDataFlavor))
		{
			try
			{
				JEditRichText data = (JEditRichText) transferable.getTransferData(JEditDataFlavor.jEditRichTextDataFlavor);
				mode = data.getMode();
				selection = data.getText();
			}
			catch (UnsupportedFlavorException | IOException e)
			{
				Log.log(Log.ERROR, Registers.class, e);
			}
		}
		else if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
		{
			selection = getTextFromTransferable(transferable, DataFlavor.stringFlavor);
		}
		if(selection == null)
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		JEditBuffer buffer = textArea.getBuffer();
		applyMode(mode, buffer);
		_paste(textArea, vertical, selection, buffer);
	}

	/**
	 * Inserts the contents of the specified register into the text area.
	 * @param textArea The text area
	 * @param register The register
	 * @param vertical Vertical (columnar) paste
	 * @param preferredDataFlavor the preferred dataflavor. If not available
	 * {@code DataFlavor.stringFlavor} will be used
	 * @since jEdit 4.4pre1
	 */
	public static void paste(TextArea textArea, char register,
		boolean vertical, DataFlavor preferredDataFlavor)
	{
		if (JEditDataFlavor.jEditRichTextDataFlavor.equals(preferredDataFlavor))
		{
			paste(textArea,register,vertical);
			return;
		}
		if(!textArea.isEditable())
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}

		Register reg = getRegister(register);

		if(reg == null)
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		Transferable transferable = reg.getTransferable();
		String selection = null;
		if (transferable.isDataFlavorSupported(preferredDataFlavor))
		{
			selection = getTextFromTransferable(transferable, preferredDataFlavor);
		}
		else if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
		{
			selection = getTextFromTransferable(transferable, DataFlavor.stringFlavor);
		}
		if(selection == null)
		{
			javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		JEditBuffer buffer = textArea.getBuffer();
		 /*
		 Commented because it must not use jEdit class.
		 Need to rewrite a property manager that is independant
		String mime = preferredDataFlavor.getMimeType();
		int i = mime.indexOf(';');
		if (i != -1)
		{
			mime = mime.substring(0,i);
		}
		String mode = jEdit.getProperty("mime2mode."+mime);
		if (mode != null)
		{
			Mode _mode = ModeProvider.instance.getMode(mode);
			if (_mode != null)
			{
				applyMode(_mode, buffer);
			}
		}     */
		_paste(textArea, vertical, selection, buffer);
	}

	private static void _paste(TextArea textArea, boolean vertical, String selection, JEditBuffer buffer)
	{
		try
		{
			buffer.beginCompoundEdit();

			/* vertical paste */
			if(vertical && textArea.getSelectionCount() == 0)
			{
				int caret = textArea.getCaretPosition();
				int caretLine = textArea.getCaretLine();
				Selection.Rect rect = new Selection.Rect(
					caretLine,caret,caretLine,caret);
				textArea.setSelectedText(rect,selection);
				caretLine = textArea.getCaretLine();

				if(caretLine != textArea.getLineCount() - 1)
				{

					int startColumn = rect.getStartColumn(
						buffer);
					int offset = buffer
						.getOffsetOfVirtualColumn(
						caretLine + 1,startColumn,null);
					if(offset == -1)
					{
						buffer.insertAtColumn(caretLine + 1,startColumn,"");
						textArea.setCaretPosition(
							buffer.getLineEndOffset(
							caretLine + 1) - 1);
					}
					else
					{
						textArea.setCaretPosition(
							buffer.getLineStartOffset(
							caretLine + 1) + offset);
					}
				}
			}
			else /* Regular paste */
			{
				textArea.replaceSelection(selection);
			}
		}
		finally
		{
			buffer.endCompoundEdit();
		}

		HistoryModel.getModel("clipboard").addItem(selection);
	} //}}}

	//{{{ applyMode() method
	private static void applyMode(Mode mode, JEditBuffer buffer)
	{
		if (mode != null &&
			"text".equals(buffer.getMode().getName()) &&
		!mode.equals(buffer.getMode()) &&
		buffer.getLength() == 0)
		{
			buffer.setMode(mode);
		}
	} //}}}

	//{{{ getTextFromTransferable() method
	private static String getTextFromTransferable(Transferable transferable, DataFlavor dataFlavor)
	{
		try
		{
			Object data = transferable.getTransferData(dataFlavor);
			return stripEOLChars(data.toString());
		}
		catch (UnsupportedFlavorException | IOException e)
		{
			Log.log(Log.ERROR, Registers.class, e);
		}
		return null;
	} //}}}

	//{{{ getRegister() method
	/**
	 * Returns the specified register.
	 * @param name The name
	 */
	public static Register getRegister(char name)
	{
		if(name != '$' && name != '%')
		{
			if(!loaded)
				loadRegisters();
		}

		if(registers == null || name >= registers.length)
			return null;
		else
			return registers[name];
	} //}}}

	//{{{ setRegister() methods
	/**
	 * Sets the specified register.
	 * @param name The name
	 * @param newRegister The new value
	 */
	public static void setRegister(char name, Register newRegister)
	{
		touchRegister(name);

		if(name >= registers.length)
		{
			registers = Arrays.copyOf(registers, Math.min(1<<16, name<<1));
		}

		registers[name] = newRegister;
		if (listener != null)
			listener.registerChanged(name);
	}

	/**
	 * Sets the specified register.
	 * @param name The name
	 * @param value The new value
	 */
	public static void setRegister(char name, String value)
	{
		setRegister(name, new StringSelection(value));
	}

	/**
	 * Sets the specified register.
	 * @param name The name
	 * @param transferable the transferable
	 */
	public static void setRegister(char name, Transferable transferable)
	{
		touchRegister(name);
		Register register = getRegister(name);
		if(register != null)
		{
			register.setTransferable(transferable);
			if (listener != null)
				listener.registerChanged(name);
		}
		else
		{
			Register defaultRegister = new DefaultRegister();
			defaultRegister.setTransferable(transferable);
			setRegister(name, defaultRegister);
		}
	} //}}}

	//{{{ clearRegister() method
	/**
	 * Sets the value of the specified register to <code>null</code>.
	 * @param name The register name
	 */
	public static void clearRegister(char name)
	{
		if(name >= registers.length)
			return;

		Register register = registers[name];
		if(name == '$' || name == '%')
			register.setTransferable(new StringSelection(""));
		else
		{
			registers[name] = null;
			modified = true;
			if (listener != null)
				listener.registerChanged(name);
		}
	} //}}}

	//{{{ getRegisters() method
	/**
	 * Returns an array of all available registers. Some of the elements
	 * of this array might be <code>null</code>.
	 */
	public static Register[] getRegisters()
	{
		if(!loaded)
			loadRegisters();
		return registers;
	} //}}}

	//{{{ getRegisterNameString() method
	/**
	 * Returns a string of all defined registers, used by the status bar
	 * (eg, "a b $ % ^").
	 * @since jEdit 4.2pre2
	 */
	@Nullable
	public static String getRegisterNameString()
	{
		if(!loaded)
			loadRegisters();

		StringBuilder buf = new StringBuilder(registers.length << 1);
		for(int i = 0; i < registers.length; i++)
		{
			if(registers[i] != null)
			{
				if(buf.length() != 0)
					buf.append(' ');
				buf.append((char)i);
			}
		}

		if(buf.length() == 0)
			return null;
		else
			return buf.toString();
	} //}}}

	//{{{ saveRegisters() method
	public static void saveRegisters()
	{
		if(!loaded || !modified)
			return;

		if (saver != null)
		{
			saver.saveRegisters();
			modified = false;
		}
	} //}}}

	//{{{ setListener() method
	public static void setListener(RegistersListener listener)
	{
		Registers.listener = listener;
	} //}}}

	//{{{ setSaver() method
	public static void setSaver(RegisterSaver saver)
	{
		Registers.saver = saver;
	} //}}}

	//{{{ isLoading() method
	public static boolean isLoading()
	{
		return loading;
	} //}}}

	//{{{ setLoading() method
	public static void setLoading(boolean loading)
	{
		Registers.loading = loading;
	} //}}}

	//{{{ Private members
	private static Register[] registers;
	private static boolean loaded, loading;
	private static RegisterSaver saver;
	private static RegistersListener listener;
	/**
	 * Flag that tell if a register has been modified (except for '%' and '$' registers that aren't
	 * saved to the xml file).
	 */
	private static boolean modified;

	private Registers() {}

	static
	{
		registers = new Register[256];
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		registers['$'] = new ClipboardRegister(toolkit.getSystemClipboard());
		Clipboard selection = toolkit.getSystemSelection();
		if(selection != null)
			registers['%'] = new ClipboardRegister(selection);
	}

	//{{{ touchRegister() method
	private static void touchRegister(char name)
	{
		if(name == '%' || name == '$')
			return;

		if(!loaded)
			loadRegisters();

		if(!loading)
			modified = true;
	} //}}}

	//{{{ loadRegisters() method
	private static void loadRegisters()
	{
		if (saver != null)
		{
			loaded = true;
			saver.loadRegisters();
		}
	} //}}}

	//{{{ loadRegisters() method
	private static String stripEOLChars(String selection) throws IOException
	{
		boolean trailingEOL = selection.endsWith("\n")
				      || selection.endsWith(System.getProperty(
			"line.separator"));

		// Some Java versions return the clipboard
		// contents using the native line separator,
		// so have to convert it here
		BufferedReader in = new BufferedReader(
			new StringReader(selection));
		StringBuilder buf = new StringBuilder(selection.length());
		String line;
		while((line = in.readLine()) != null)
		{
			// broken Eclipse workaround!
			// 24 Febuary 2004
			if(line.endsWith("\0"))
			{
				line = line.substring(0,
						      line.length() - 1);
			}
			buf.append(line);
			buf.append('\n');
		}
		// remove trailing \n
		if(!trailingEOL && buf.length() != 0)
			buf.setLength(buf.length() - 1);
		return buf.toString();
	}  //}}}

	//}}}

	//{{{ Inner classes

	//{{{ Register interface
	/**
	 * A register.
	 */
	public interface Register
	{
		/**
		 * Converts to a string.
		 */
		//@Deprecated
		// undeprecating this since the two concrete classes defined in Registers
		// use it and several other classes depend on it.
		// TODO: finish the work to actually deprecate this.
		String toString();

		/**
		 * Sets the register contents.
		 * @deprecated use {@link #setTransferable(java.awt.datatransfer.Transferable)}
		 * instead, for example
		 * <code>setTransferable(new StringSelection(""))</code>
		 */
		@Deprecated
		void setValue(String value);

		Transferable getTransferable();

		void setTransferable(Transferable transferable);
	} //}}}

	//{{{ ClipboardRegister class
	/**
	 * A clipboard register. Register "$" should always be an
	 * instance of this.
	 */
	public static class ClipboardRegister implements Register
	{
		Clipboard clipboard;

		public ClipboardRegister(Clipboard clipboard)
		{
			this.clipboard = clipboard;
		}

		/**
		 * Sets the clipboard contents.
		 */
		@Override
		public void setValue(String value)
		{
			Transferable selection = new StringSelection(value);
			clipboard.setContents(selection, null);
		}

		/**
		 * Returns the clipboard contents.
		 */
		@Override
		public String toString()
		{
			try
			{

				if (false)
				{
					/*
						This is to debug clipboard problems.

						Apparently, jEdit is unable to copy text from clipbard into the current
						text buffer if the clipboard was filled using the command
							echo test | xselection CLIPBOARD -
						under Linux. However, it seems that Java does not offer any
						data flavor for this clipboard content (under J2RE 1.5.0_06-b05)
						Thus, copying from clipboard seems to be plainly impossible.
					*/
					Log.log(Log.DEBUG,this,"clipboard.getContents(this)="+clipboard.getContents(this)+'.');
					debugListDataFlavors(clipboard.getContents(this));
				}

				String selection = (String)clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor);

				return stripEOLChars(selection);
			}
			catch(Exception e)
			{
				Log.log(Log.NOTICE,this,e);
				return null;
			}
		}

		@Override
		public Transferable getTransferable()
		{
			return clipboard.getContents(this);
		}

		@Override
		public void setTransferable(Transferable transferable)
		{
			clipboard.setContents(transferable, null);
		}
	} //}}}

	//{{{ debugListDataFlavors() method
	protected static void debugListDataFlavors(Transferable transferable)
	{
		DataFlavor[] dataFlavors = transferable.getTransferDataFlavors();

		for (DataFlavor dataFlavor : dataFlavors)
		{
			Log.log(Log.DEBUG, Registers.class, "debugListDataFlavors(): dataFlavor=" +
				dataFlavor + '.');
		}

		if (dataFlavors.length == 0)
		{
			Log.log(Log.DEBUG,Registers.class,
				"debugListDataFlavors(): no dataFlavor supported.");
		}
	} //}}}

	//{{{ DefaultRegister class
	private static class DefaultRegister implements Register
	{
		private Transferable transferable;

		@Override
		public void setValue(String value)
		{
			transferable = new StringSelection(value);
		}

		@Override
		public String toString()
		{
			if (transferable == null)
				return null;
			if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
			{
				try
				{
					return transferable.getTransferData(DataFlavor.stringFlavor).toString();
				}
				catch (UnsupportedFlavorException | IOException e)
				{
					Log.log(Log.ERROR, this, e);
				}
			}
			return transferable.toString();
		}

		@Override
		public Transferable getTransferable()
		{
			return transferable;
		}

		@Override
		public void setTransferable(Transferable transferable)
		{
			this.transferable = transferable;
		}
	} //}}}

	//}}}
}
