/*
 * Selection.java - Selected text
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 2001, 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.textarea;

//{{{ Imports
import java.util.ArrayList;
import java.util.List;

import org.gjt.sp.jedit.buffer.JEditBuffer;
import org.gjt.sp.util.Log;
import org.gjt.sp.util.StandardUtilities;
//}}}

/**
 * An abstract class that holds data on a region of selected text.
 * As an abstract class, it cannot be used
 * directly, but instead serves as a parent class for two specific types
 * of selection structures:
 * <ul>
 * <li>{@link Selection.Range} - represents an ordinary range of selected text.</li>
 * <li>{@link Selection.Rect} - represents a rectangular selection.</li>
 * </ul>
 *
 * @author Slava Pestov
 * @author John Gellene (API documentation)
 * @version $Id: Selection.java 25283 2020-04-22 08:37:19Z kpouer $
 * @since jEdit 3.2pre1
 */
public abstract class Selection implements Cloneable
{
	//{{{ getStart() method
	/**
	 * Returns the start offset of this selection.
	 */
	public int getStart()
	{
		return start;
	} //}}}

	//{{{ getEnd() method
	/**
	 * Returns the end offset of this selection.
	 */
	public int getEnd()
	{
		return end;
	} //}}}

	//{{{ getStart() method
	/**
	 * Returns the beginning of the portion of the selection
	 * falling on the specified line. Used to manipulate
	 * selection text on a line-by-line basis.
	 * @param buffer The buffer
	 * @param line The line number
	 * @since jEdit 4.1pre1
	 */
	public abstract int getStart(JEditBuffer buffer, int line);
	//}}}

	//{{{ getEnd() method
	/**
	 * Returns the end of the portion of the selection
	 * falling on the specified line. Used to manipulate
	 * selection text on a line-by-line basis.
	 * @param buffer The buffer
	 * @param line The line number
	 * @since jEdit 4.1pre1
	 */
	public abstract int getEnd(JEditBuffer buffer, int line);
	//}}}

	//{{{ getStartLine() method
	/**
	 * Returns the starting line number of this selection.
	 */
	public int getStartLine()
	{
		return startLine;
	} //}}}

	//{{{ getEndLine() method
	/**
	 * Returns the ending line number of this selection.
	 */
	public int getEndLine()
	{
		return endLine;
	} //}}}

	//{{{ overlaps() method
	/**
	 * Returns if this selection and the specified selection overlap.
	 * @param s The other selection
	 * @since jEdit 4.1pre1
	 */
	public boolean overlaps(Selection s)
	{
		if((start >= s.start && start <= s.end)
			|| (end >= s.start && end <= s.end))
			return true;
		else
			return false;
	} //}}}

	//{{{ toString() method
	@Override
	public String toString()
	{
		return getClass().getName() + "[start=" + start
			+ ",end=" + end + ",startLine=" + startLine
			+ ",endLine=" + endLine + ']';
	} //}}}

	//{{{ clone() method
	@Override
	public Object clone()
	{
		try
		{
			return super.clone();
		}
		catch(CloneNotSupportedException e)
		{
			throw new InternalError("I just drank a whole "
				+ "bottle of cough syrup and I feel "
				+ "funny!");
		}
	} //}}}

	//{{{ Package-private members
	int start, end;
	int startLine, endLine;

	//{{{ Selection constructor
	protected Selection()
	{
	} //}}}

	//{{{ Selection constructor
	protected Selection(Selection sel)
	{
		this.start = sel.start;
		this.end = sel.end;
	} //}}}

	//{{{ Selection constructors
	protected Selection(int start, int end)
	{
		this.start = start;
		this.end = end;
	}

	protected Selection(int startLine, int start, int endLine, int end)
	{
		this.startLine = startLine;
		this.start     = start;
		this.endLine   = endLine;
		this.end       = end;
	} //}}}

	// should the next two be public, maybe?
	abstract void getText(JEditBuffer buffer, StringBuilder buf);

	/**
	 * Replace the selection with the given text
	 * @param buffer the buffer
	 * @param text the text
	 * @return the offset at the end of the inserted text
	 */
	abstract int setText(JEditBuffer buffer, String text);

	/**
	 * Called when text is inserted into the buffer.
	 * @param buffer The buffer in question
	 * @param startLine The first line
	 * @param start The start offset, from the beginning of the buffer
	 * @param numLines The number of lines inserted
	 * @param length The number of characters inserted
	 * @return <code>true</code> if the selection was changed
	 */
	abstract boolean contentInserted(JEditBuffer buffer, int startLine, int start,
		int numLines, int length);

	/**
	 * Called when text is removed from the buffer.
	 * @param buffer The buffer in question
	 * @param startLine The first line
	 * @param start The start offset, from the beginning of the buffer
	 * @param numLines The number of lines removed
	 * @param length The number of characters removed
	 * @return <code>true</code> if the selection was changed
	 */
	abstract boolean contentRemoved(JEditBuffer buffer, int startLine, int start,
		int numLines, int length);
	//}}}

	//{{{ Range class
	/**
	 * An ordinary range selection.
	 * @since jEdit 3.2pre1
	 */
	public static class Range extends Selection
	{
		//{{{ Range constructor
		public Range()
		{
		} //}}}

		//{{{ Range constructor
		public Range(Selection sel)
		{
			super(sel);
		} //}}}

		//{{{ Range constructor
		public Range(int start, int end)
		{
			super(start,end);
		} //}}}

		//{{{ getStart() method
		@Override
		public int getStart(JEditBuffer buffer, int line)
		{
			if(line == startLine)
				return start;
			else
				return buffer.getLineStartOffset(line);
		} //}}}

		//{{{ getEnd() method
		@Override
		public int getEnd(JEditBuffer buffer, int line)
		{
			if(line == endLine)
				return end;
			else
				return buffer.getLineEndOffset(line) - 1;
		} //}}}

		//{{{ Package-private members

		//{{{ getText() method
		@Override
		void getText(JEditBuffer buffer, StringBuilder buf)
		{
			buf.append(buffer.getText(start,end - start));
		} //}}}

		//{{{ setText() method
		/**
		 * Replace the selection with the given text
		 * @param buffer the buffer
		 * @param text the text
		 * @return the offset at the end of the inserted text
		 */
		@Override
		int setText(JEditBuffer buffer, String text)
		{
			buffer.remove(start,end - start);
			if(text != null && !text.isEmpty())
			{
				buffer.insert(start,text);
				return start + text.length();
			}
			else
				return start;
		} //}}}

		//{{{ contentInserted() method
		@Override
		boolean contentInserted(JEditBuffer buffer, int startLine, int start,
			int numLines, int length)
		{
			boolean changed = false;

			if(this.start >= start)
			{
				this.start += length;
				if(numLines != 0)
					this.startLine = buffer.getLineOfOffset(this.start);
				changed = true;
			}

			if(this.end >= start)
			{
				this.end += length;
				if(numLines != 0)
					this.endLine = buffer.getLineOfOffset(this.end);
				changed = true;
			}

			return changed;
		} //}}}

		//{{{ contentRemoved() method
		@Override
		boolean contentRemoved(JEditBuffer buffer, int startLine, int start,
			int numLines, int length)
		{
			int end = start + length;
			boolean changed = false;

			if(this.start > start && this.start <= end)
			{
				this.start = start;
				changed = true;
			}
			else if(this.start > end)
			{
				this.start -= length;
				changed = true;
			}

			if(this.end > start && this.end <= end)
			{
				this.end = start;
				changed = true;
			}
			else if(this.end > end)
			{
				this.end -= length;
				changed = true;
			}

			if(changed && numLines != 0)
			{
				this.startLine = buffer.getLineOfOffset(this.start);
				this.endLine = buffer.getLineOfOffset(this.end);
			}

			return changed;
		} //}}}

		//}}}
	} //}}}

	//{{{ Rect class
	/**
	 * A rectangular selection.
	 * @since jEdit 3.2pre1
	 */
	// this class is not very fast...
	public static class Rect extends Selection
	{
		//{{{ Rect constructor
		public Rect()
		{
		} //}}}

		//{{{ Rect constructor
		public Rect(Selection sel)
		{
			super(sel);
		} //}}}

		//{{{ Rect constructor
		public Rect(int start, int end)
		{
			super(start,end);
		} //}}}

		//{{{ Rect constructor
		public Rect(int startLine, int start, int endLine, int end)
		{
			super(startLine, start, endLine, end);
		} //}}}

		//{{{ Rect constructor
		public Rect(JEditBuffer buffer, int startLine, int startColumn,
			int endLine, int endColumn)
		{
			this.startLine = startLine;
			this.endLine = endLine;

			int[] width = new int[1];
			int startOffset = buffer.getOffsetOfVirtualColumn(startLine,
				startColumn,width);
			if(startOffset == -1)
			{
				extraStartVirt = startColumn - width[0];
				startOffset = buffer.getLineEndOffset(startLine) - 1;
			}
			else
				startOffset += buffer.getLineStartOffset(startLine);

			int endOffset = buffer.getOffsetOfVirtualColumn(endLine,
				endColumn,width);
			if(endOffset == -1)
			{
				extraEndVirt = endColumn - width[0];
				endOffset = buffer.getLineEndOffset(endLine) - 1;
			}
			else
				endOffset += buffer.getLineStartOffset(endLine);
			this.start = startOffset;
			this.end = endOffset;
		} //}}}

		//{{{ getStartColumn() method
		public int getStartColumn(JEditBuffer buffer)
		{
			int virtColStart = buffer.getVirtualWidth(startLine,
				start - buffer.getLineStartOffset(startLine)) + extraStartVirt;
			int virtColEnd = buffer.getVirtualWidth(endLine,
				end - buffer.getLineStartOffset(endLine)) + extraEndVirt;
			return Math.min(virtColStart,virtColEnd);
		} //}}}

		//{{{ getEndColumn() method
		public int getEndColumn(JEditBuffer buffer)
		{
			int virtColStart = buffer.getVirtualWidth(startLine,
				start - buffer.getLineStartOffset(startLine)) + extraStartVirt;
			int virtColEnd = buffer.getVirtualWidth(endLine,
				end - buffer.getLineStartOffset(endLine)) + extraEndVirt;
			return Math.max(virtColStart,virtColEnd);
		} //}}}

		//{{{ getStart() method
		@Override
		public int getStart(JEditBuffer buffer, int line)
		{
			return getColumnOnOtherLine(buffer,line,
				getStartColumn(buffer));
		} //}}}

		//{{{ getEnd() method
		@Override
		public int getEnd(JEditBuffer buffer, int line)
		{
			return getColumnOnOtherLine(buffer,line,
				getEndColumn(buffer));
		} //}}}

		//{{{ Package-private members
		int extraStartVirt;
		int extraEndVirt;

		//{{{ getText() method
		@Override
		void getText(JEditBuffer buffer, StringBuilder buf)
		{
			int start = getStartColumn(buffer);
			int end = getEndColumn(buffer);

			for(int i = startLine; i <= endLine; i++)
			{
				int lineStart = buffer.getLineStartOffset(i);
				int lineLen = buffer.getLineLength(i);

				int rectStart = buffer.getOffsetOfVirtualColumn(
					i,start,null);
				if(rectStart == -1)
					rectStart = lineLen;

				int rectEnd = buffer.getOffsetOfVirtualColumn(
					i,end,null);
				if(rectEnd == -1)
					rectEnd = lineLen;

				if(rectEnd < rectStart)
				{
					Log.log(Log.ERROR, this, i + ":::" + start + ':' + end + " ==> " + rectStart + ':' + rectEnd);
				}
				buf.append(buffer.getText(lineStart + rectStart,
					rectEnd - rectStart));

				if(i != endLine)
					buf.append('\n');
			}
		} //}}}

		//{{{ setText() method
		/**
		 * Replace the selection with the given text
		 * @param buffer the buffer
		 * @param text the text
		 * @return the offset at the end of the inserted text
		 */
		@Override
		int setText(JEditBuffer buffer, String text)
		{
			int startColumn = getStartColumn(buffer);
			int endColumn = getEndColumn(buffer);

			int tabSize = buffer.getTabSize();

			int maxWidth = 0;
			int totalLines = 0;
			/* This list will contains Strings and Integer. */
			List<Object> lines = new ArrayList<Object>();

			//{{{ Split the text into lines
			if(text != null)
			{
				int lastNewline = 0;
				int currentWidth = startColumn;
				for(int i = 0; i < text.length(); i++)
				{
					char ch = text.charAt(i);
					if(ch == '\n')
					{
						totalLines++;
						lines.add(text.substring(
							lastNewline,i));
						lastNewline = i + 1;
						maxWidth = Math.max(maxWidth,currentWidth);
						lines.add(currentWidth);
						currentWidth = startColumn;
					}
					else if(ch == '\t')
						currentWidth += tabSize - (currentWidth % tabSize);
					else
						currentWidth++;
				}

				if(lastNewline != text.length())
				{
					totalLines++;
					lines.add(text.substring(lastNewline));
					lines.add(currentWidth);
					maxWidth = Math.max(maxWidth,currentWidth);
				}
			} //}}}

			//{{{ Insert the lines into the buffer
			int endOffset = 0;
			int[] total = new int[1];
			int lastLine = Math.max(startLine + totalLines - 1,endLine);
			for(int i = startLine; i <= lastLine; i++)
			{
				if(i == buffer.getLineCount())
					buffer.insert(buffer.getLength(),"\n");

				int lineStart = buffer.getLineStartOffset(i);
				int lineLen = buffer.getLineLength(i);

				int rectStart = buffer.getOffsetOfVirtualColumn(
					i,startColumn,total);
				int startWhitespace;
				if(rectStart == -1)
				{
					startWhitespace = startColumn - total[0];
					rectStart = lineLen;
				}
				else
					startWhitespace = 0;

				int rectEnd = buffer.getOffsetOfVirtualColumn(
					i,endColumn,null);
				if(rectEnd == -1)
					rectEnd = lineLen;

				buffer.remove(rectStart + lineStart,rectEnd - rectStart);

				if(startWhitespace != 0)
				{
					buffer.insert(rectStart + lineStart,
						StandardUtilities.createWhiteSpace(startWhitespace,0));
				}

				int endWhitespace;
				if(totalLines == 0)
				{
					if(rectEnd == lineLen)
						endWhitespace = 0;
					else
						endWhitespace = maxWidth - startColumn;
				}
				else
				{
					int index = 2 * ((i - startLine) % totalLines);
					String str = (String)lines.get(index);
					buffer.insert(rectStart + lineStart + startWhitespace,str);
					if(rectEnd == lineLen)
						endWhitespace = 0;
					else
					{
						endWhitespace = maxWidth
							- (Integer) lines.get(index + 1);
					}
					startWhitespace += str.length();
				}

				if(endWhitespace != 0)
				{
					buffer.insert(rectStart + lineStart
						+ startWhitespace,
						StandardUtilities.createWhiteSpace(endWhitespace,0));
				}

				endOffset = rectStart + lineStart
					+ startWhitespace
					+ endWhitespace;
			} //}}}

			//{{{ Move the caret down a line
			if(text == null || text.isEmpty())
				return end;
			else
				return endOffset;
			//}}}
		} //}}}

		//{{{ contentInserted() method
		@Override
		boolean contentInserted(JEditBuffer buffer, int startLine, int start,
			int numLines, int length)
		{
			if(this.end < start)
				return false;

			this.end += length;

			if(this.startLine > startLine)
			{
				this.start += length;
				if(numLines != 0)
				{
					this.startLine = buffer.getLineOfOffset(
						this.start);
					this.endLine = buffer.getLineOfOffset(
						this.end);
				}
				return true;
			}

			int endVirtualColumn = buffer.getVirtualWidth(
				this.endLine,end
				- buffer.getLineStartOffset(this.endLine));

			if(this.start == start)
			{
				int startVirtualColumn = buffer.getVirtualWidth(
					this.startLine,start
					- buffer.getLineStartOffset(
					this.startLine));

				this.start += length;

				int newStartVirtualColumn
					= buffer.getVirtualWidth(
						startLine,start -
						buffer.getLineStartOffset(
						this.startLine));

				int[] totalVirtualWidth = new int[1];
				int newEnd = buffer.getOffsetOfVirtualColumn(
					this.endLine,endVirtualColumn +
					newStartVirtualColumn -
					startVirtualColumn,
					totalVirtualWidth);

				if(newEnd != -1)
				{
					end = buffer.getLineStartOffset(
						this.endLine) + newEnd;
				}
				else
				{
					end = buffer.getLineEndOffset(
						this.endLine) - 1;
					extraEndVirt = totalVirtualWidth[0]
						- endVirtualColumn;
				}
			}
			else if(this.start > start)
			{
				this.start += length;
				if(numLines != 0)
				{
					this.startLine = buffer.getLineOfOffset(
						this.start);
				}
			}

			if(numLines != 0)
				this.endLine = buffer.getLineOfOffset(this.end);
			int newEndVirtualColumn = buffer.getVirtualWidth(
				endLine,
				end - buffer.getLineStartOffset(this.endLine));
			if(startLine == this.endLine && extraEndVirt != 0)
			{
				extraEndVirt += endVirtualColumn - newEndVirtualColumn;
			}
			else if(startLine == this.startLine
				&& extraStartVirt != 0)
			{
				extraStartVirt += endVirtualColumn - newEndVirtualColumn;
			}

			return true;
		} //}}}

		//{{{ contentRemoved() method
		@Override
		boolean contentRemoved(JEditBuffer buffer, int startLine, int start,
			int numLines, int length)
		{
			int end = start + length;
			boolean changed = false;

			if(this.start > start && this.start <= end)
			{
				this.start = start;
				changed = true;
			}
			else if(this.start > end)
			{
				this.start -= length;
				changed = true;
			}

			if(this.end > start && this.end <= end)
			{
				this.end = start;
				changed = true;
			}
			else if(this.end > end)
			{
				this.end -= length;
				changed = true;
			}

			if(changed && numLines != 0)
			{
				this.startLine = buffer.getLineOfOffset(this.start);
				this.endLine = buffer.getLineOfOffset(this.end);
			}

			return changed;
		} //}}}

		//}}}

		//{{{ Private members

		//{{{ getColumnOnOtherLine() method
		private static int getColumnOnOtherLine(JEditBuffer buffer, int line,
			int col)
		{
			int returnValue = buffer.getOffsetOfVirtualColumn(
				line,col,null);
			if(returnValue == -1)
				return buffer.getLineEndOffset(line) - 1;
			else
				return buffer.getLineStartOffset(line) + returnValue;
		} //}}}

		//}}}
	} //}}}
}
