/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.viewer.text;

import java.awt.Font;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;

import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;

import com.mucommander.commons.util.StringUtils;
import com.mucommander.job.impl.SearchJob;
import com.mucommander.ui.theme.ColorChangedEvent;
import com.mucommander.ui.theme.FontChangedEvent;
import com.mucommander.ui.theme.Theme;
import com.mucommander.ui.theme.ThemeListener;
import com.mucommander.ui.theme.ThemeManager;

/**
 * Text editor implementation used by {@link TextViewer} and {@link TextEditor}.
 *
 * @author Maxence Bernard, Mariusz Jakubowski, Nicolas Rinaudo, Arik Hadas
 */
class TextEditorImpl implements ThemeListener {

	private JFrame frame;

	private JTextArea textArea;

	/** Indicates whether there is a line separator in the original file */
	private boolean lineSeparatorExists;

	////////////////////
	// Initialization //
	////////////////////

	public TextEditorImpl(boolean isEditable) {
		// Initialize text area
		initTextArea(isEditable);

		// Listen to theme changes to update the text area if it is visible
		ThemeManager.addCurrentThemeListener(this);
	}

	private void initTextArea(boolean isEditable) {
		textArea = new JTextArea() {
			@Override
			public Insets getInsets() {
				return new Insets(4, 3, 4, 3);
			}
		};

		textArea.setEditable(isEditable);
		// Use theme colors and font
		textArea.setForeground(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR));
		textArea.setCaretColor(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR));
		textArea.setBackground(ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR));
		textArea.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_FOREGROUND_COLOR));
		textArea.setSelectionColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_BACKGROUND_COLOR));
		textArea.setFont(ThemeManager.getCurrentFont(Theme.EDITOR_FONT));

		textArea.setWrapStyleWord(true);

		textArea.addMouseWheelListener(new MouseWheelListener() {

			/**
			 * Mouse events bubble up until finding a component with a relative listener.
			 * That's why in case we get an event that needs to initiate its default behavior,
			 * we just bubble it up to the parent component of the JTextArea.  
			 */
			public void mouseWheelMoved(MouseWheelEvent e) {
				boolean isCtrlPressed = (e.getModifiers() & KeyEvent.CTRL_MASK)!=0;
				if (isCtrlPressed) {
					Font currentFont = textArea.getFont();
					int currentFontSize = currentFont.getSize();
					boolean rotationUp = e.getWheelRotation() < 0;
					if ((!rotationUp && currentFontSize > 1) || rotationUp) {
						Font newFont = new Font(currentFont.getName(), currentFont.getStyle(), currentFontSize + (rotationUp ? 1 : -1));
						textArea.setFont(newFont);
					}
				}
				else {
					textArea.getParent().dispatchEvent(e);
				}
			}
		});
	}

	/////////////////
	// Search code //
	/////////////////

	void find() {
		FindDialog findDialog = new FindDialog(frame);

		if (findDialog.wasValidated()) {
		    String searchString = findDialog.getSearchString().toLowerCase();

		    if (!StringUtils.isNullOrEmpty(searchString)) {
		        SearchJob.lastSearchString = searchString;
		        doSearch(0, true);
		    }
		}
	}


	void findNext() {
	    if (StringUtils.isNullOrEmpty(SearchJob.lastSearchString))
	        find();
	    else
	        doSearch(textArea.getSelectionEnd(), true);
	}

	void findPrevious() {
		doSearch(textArea.getSelectionStart() - 1, false);
	}

	private String getTextLC() {
		return textArea.getText().toLowerCase();
	}

	private void doSearch(int startPos, boolean forward) {
	    String searchString = SearchJob.lastSearchString;
	    if (StringUtils.isNullOrEmpty(searchString))
	        return;

		textArea.requestFocus();

		int pos;
		if (forward) {
			pos = getTextLC().indexOf(searchString, startPos);
		} else {
			pos = getTextLC().lastIndexOf(searchString, startPos);
		}
		if (pos >= 0) {
			textArea.select(pos, pos + searchString.length());
		} else {
			// Beep when no match has been found.
			// The beep method is called from a separate thread because this method seems to lock until the beep has
			// been played entirely. If the 'Find next' shortcut is left pressed, a series of beeps will be played when
			// the end of the file is reached, and we don't want those beeps to played one after the other as to:
			// 1/ not lock the event thread
			// 2/ have those beeps to end rather sooner than later
			new Thread(() -> Toolkit.getDefaultToolkit().beep()).start();
		}
	}

	public boolean isWrap() {
		return textArea.getLineWrap();
	}

	////////////////////////////
	// Package-access methods //
	////////////////////////////

	void wrap(boolean isWrap) {
		textArea.setLineWrap(isWrap);
		textArea.repaint();
	}

	void copy() {
		textArea.copy();
	}

	void cut() {
		textArea.cut();
	}

	void paste() {
		textArea.paste();
	}

	void selectAll() {
		textArea.selectAll();
	}

	void requestFocus() {
		textArea.requestFocus();
	}

	JTextArea getTextArea() {
		return textArea;
	}

	void addDocumentListener(DocumentListener documentListener) {
		textArea.getDocument().addDocumentListener(documentListener);
	}

	void read(Reader reader) throws IOException {
		// Feed the file's contents to text area
		textArea.read(reader, null);

		// If there are more than one lines, there is a line separator
		lineSeparatorExists = textArea.getLineCount() > 1;

		// Move cursor to the top
		textArea.setCaretPosition(0);
	}

	void write(Writer writer) throws IOException {
		Document document = textArea.getDocument();

		// According to the documentation in DefaultEditorKit, the line separator is set to be as the system property
		// if no other line separator exists in the file, but in practice it is not, so this is a workaround for it
		if (!lineSeparatorExists)
			document.putProperty(DefaultEditorKit.EndOfLineStringProperty, System.getProperty("line.separator"));

		try {
			textArea.getUI().getEditorKit(textArea).write(new BufferedWriter(writer), document, 0, document.getLength());
		}
		catch(BadLocationException e) {
			throw new IOException(e.getMessage());
		}
	}

	//////////////////////////////////
	// ThemeListener implementation //
	//////////////////////////////////

	/**
	 * Receives theme color changes notifications.
	 */
	public void colorChanged(ColorChangedEvent event) {
		switch(event.getColorId()) {
		case Theme.EDITOR_FOREGROUND_COLOR:
			textArea.setForeground(event.getColor());
			break;

		case Theme.EDITOR_BACKGROUND_COLOR:
			textArea.setBackground(event.getColor());
			break;

		case Theme.EDITOR_SELECTED_FOREGROUND_COLOR:
			textArea.setSelectedTextColor(event.getColor());
			break;

		case Theme.EDITOR_SELECTED_BACKGROUND_COLOR:
			textArea.setSelectionColor(event.getColor());
			break;
		}
	}

	/**
	 * Receives theme font changes notifications.
	 */
	public void fontChanged(FontChangedEvent event) {
		if(event.getFontId() == Theme.EDITOR_FONT)
			textArea.setFont(event.getFont());
	}
}
