/**
 * Input/output package
 */
package kr.jihee.text_toolkit.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import kr.jihee.text_toolkit.lang.JFunction.Func;
import kr.jihee.text_toolkit.lang.JFunction.Thrower;
import kr.jihee.text_toolkit.lang.JObject.StrCaster;

/**
 * Class for handling text file
 * 
 * @author Jihee
 */
public class JText {
	public static String DEFAULT_CHARSET = "UTF-8";
	public static String DEFAULT_LINE_DELIMITER = "\n";
	public static boolean DEFAULT_APPEND = false;
	public static final Collector<CharSequence, ?, String> joiner = Collectors.joining(DEFAULT_LINE_DELIMITER);

	/**
	 * Class for writing text to a file
	 * 
	 * @author Jihee
	 */
	public static class TextWriter implements Closeable, Flushable {
		Optional<StringWriter> texter;
		BufferedWriter writer;
		File file;

		private TextWriter() {
		}

		public TextWriter to(File file) {
			this.file = file;
			return this;
		}

		private static TextWriter of(Writer writer) {
			TextWriter self = new TextWriter();
			self.texter = writer instanceof StringWriter ? Optional.of((StringWriter) writer) : Optional.empty();
			self.writer = new BufferedWriter(writer);
			return self;
		}

		public static TextWriter of() {
			return of(new StringWriter());
		}

		public static TextWriter of(OutputStream output) {
			try {
				return of(new OutputStreamWriter(output, DEFAULT_CHARSET));
			} catch (Throwable e) {
				Thrower.throwing(e);
				return null;
			}
		}

		public static TextWriter of(File file, boolean append) {
			try {
				return of(new FileOutputStream(file, append)).to(file);
			} catch (Throwable e) {
				Thrower.throwing(e);
				return null;
			}
		}

		public static TextWriter of(File file) {
			return of(file, DEFAULT_APPEND);
		}

		public Optional<String> toText() {
			return texter.map(StringWriter::toString);
		}

		public void close() {
			try {
				writer.close();
			} catch (IOException e) {
				Thrower.throwing(e);
			}
		}

		public void flush() {
			try {
				writer.flush();
			} catch (IOException e) {
				Thrower.throwing(e);
			}
		}

		public TextWriter write(Object value) {
			try {
				writer.write(StrCaster.to(value));
			} catch (Throwable e) {
				Thrower.throwing(e);
			}
			return this;
		}

		public TextWriter writeln(Object value) {
			try {
				writer.write(StrCaster.to(value));
				writer.newLine();
			} catch (Throwable e) {
				Thrower.throwing(e);
			}
			return this;
		}

		public TextWriter writeln(Stream<?> values) {
			values.forEachOrdered(this::writeln);
			return this;
		}

		public TextWriter print(Object value) {
			write(value).flush();
			return this;
		}

		public TextWriter println(Object value) {
			writeln(value).flush();
			return this;
		}

		public TextWriter println(Stream<?> values) {
			writeln(values).flush();
			return this;
		}

		public TextWriter push(Object value) {
			TextWriter.of(file, true).write(value).close();
			return this;
		}

		public TextWriter pushln(Object value) {
			TextWriter.of(file, true).writeln(value).close();
			return this;
		}

		public TextWriter pushln(Stream<?> values) {
			TextWriter.of(file, true).writeln(values).close();
			return this;
		}

		public TextWriter clear() {
			TextWriter.of(file, false).close();
			return this;
		}

		public void replaceAll(TextReader reader, Pattern pattern, String replacement) {
			writeln(reader.lines().map(Func.replaceAll(pattern, replacement))).close();
		}

		public void replaceValue(TextReader reader, String name, String value) {
			replaceAll(reader, Pattern.compile(String.format("^(%s\\s*=\\s*).*", Pattern.quote(name)), Pattern.MULTILINE), "$1" + value);
		}
		
		public static void write(File file, Object value) {
			TextWriter.of(file).write(value).close();
		}
		
		public static void writeln(File file, Object value) {
			TextWriter.of(file).writeln(value).close();
		}
		
		public static void push(File file, Object value) {
			TextWriter.of(file, true).write(value).close();
		}
		
		public static void pushln(File file, Object value) {
			TextWriter.of(file, true).writeln(value).close();
		}
	}

	/**
	 * Class for reading text from a file
	 * 
	 * @author Jihee
	 */
	public static class TextReader implements Closeable {
		BufferedReader reader;

		private TextReader() {
		}

		private static TextReader of(Reader reader) {
			TextReader self = new TextReader();
			self.reader = new BufferedReader(reader);
			return self;
		}

		public static TextReader of(InputStream input) {
			try {
				return of(new InputStreamReader(input, DEFAULT_CHARSET));
			} catch (Throwable e) {
				Thrower.throwing(e);
				return null;
			}
		}

		public static TextReader of(File file) {
			try {
				return of(new FileInputStream(file));
			} catch (Throwable e) {
				Thrower.throwing(e);
				return null;
			}
		}

		public static TextReader of(String text) {
			return of(new StringReader(text));
		}

		public void close() {
			try {
				reader.close();
			} catch (Throwable e) {
				Thrower.throwing(e);
			}
		}

		public Stream<String> lines() {
			try {
				return reader.lines().onClose(() -> asRunnable(reader));
			} catch (Throwable e) {
				asRunnable(reader).run();
				Thrower.throwing(e);
				return Stream.empty();
			}
		}

		public String read() {
			return lines().collect(joiner);
		}

		public static Stream<String> lines(InputStream input) {
			return of(input).lines();
		}

		public static Stream<String> lines(File file) {
			return of(file).lines();
		}

		public static Stream<String> lines(String text) {
			return of(text).lines();
		}

		public static String read(InputStream input) {
			return of(input).read();
		}

		public static String read(File file) {
			return of(file).read();
		}

		public static void pause() {
			try {
				new BufferedReader(new InputStreamReader(System.in)).readLine();
			} catch (Throwable e) {
				Thrower.throwing(e);
			}
		}
	}

	public static Runnable asRunnable(Closeable c) {
		return () -> {
			try {
				c.close();
			} catch (Throwable e) {
				Thrower.throwing(e);
			}
		};
	}
}
