package com.github.davidmoten.rx;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import com.github.davidmoten.rx.internal.operators.OnSubscribeReader;
import com.github.davidmoten.util.Preconditions;

import rx.Observable;
import rx.functions.Action1;
import rx.functions.Action2;
import rx.functions.Func0;
import rx.functions.Func1;

public final class Strings {

    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    /**
     * Returns null if input is null otherwise returns input.toString().trim().
     */
    private static Func1<Object, String> TRIM = new Func1<Object, String>() {

        @Override
        public String call(Object input) {
            if (input == null)
                return null;
            else
                return input.toString().trim();
        }
    };

    @SuppressWarnings("unchecked")
    public static <T> Func1<T, String> trim() {
        return (Func1<T, String>) TRIM;
    }

    public static Observable<String> from(Reader reader, int bufferSize) {
        return Observable.create(new OnSubscribeReader(reader, bufferSize));
    }

    public static Observable<String> from(Reader reader) {
        return from(reader, 8192);
    }

    public static Observable<String> from(InputStream is) {
        return from(new InputStreamReader(is));
    }

    public static Observable<String> from(InputStream is, Charset charset) {
        return from(new InputStreamReader(is, charset));
    }

    public static Observable<String> from(InputStream is, Charset charset, int bufferSize) {
        return from(new InputStreamReader(is, charset), bufferSize);
    }

    public static Observable<String> split(Observable<String> source, String pattern) {
        return source.compose(Transformers.split(pattern));
    }

    public static Observable<String> concat(Observable<String> source) {
        return join(source, "");
    }

    public static Observable<String> concat(Observable<String> source, final String delimiter) {
        return join(source, delimiter);
    }

    public static Observable<String> strings(Observable<?> source) {
        return source.map(new Func1<Object, String>() {
            @Override
            public String call(Object t) {
                return String.valueOf(t);
            }
        });
    }

    public static Observable<String> from(File file) {
        return from(file, DEFAULT_CHARSET);
    }

    public static Observable<String> from(final File file, final Charset charset) {
        Preconditions.checkNotNull(file);
        Preconditions.checkNotNull(charset);
        Func0<Reader> resourceFactory = new Func0<Reader>() {
            @Override
            public Reader call() {
                try {
                    return new InputStreamReader(new FileInputStream(file), charset);
                } catch (FileNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        return from(resourceFactory);
    }

    public static Observable<String> fromClasspath(final String resource, final Charset charset) {
        Preconditions.checkNotNull(resource);
        Preconditions.checkNotNull(charset);
        Func0<Reader> resourceFactory = new Func0<Reader>() {
            @Override
            public Reader call() {
                return new InputStreamReader(Strings.class.getResourceAsStream(resource), charset);
            }
        };
        return from(resourceFactory);
    }

    public static Observable<String> fromClasspath(final String resource) {
        return fromClasspath(resource, Utf8Holder.INSTANCE);
    }

    private static class Utf8Holder {
        static final Charset INSTANCE = Charset.forName("UTF-8");
    }

    public static Observable<String> from(final Func0<Reader> readerFactory) {
        Func1<Reader, Observable<String>> observableFactory = new Func1<Reader, Observable<String>>() {
            @Override
            public Observable<String> call(Reader reader) {
                return from(reader);
            }
        };
        return Observable.using(readerFactory, observableFactory, DisposeActionHolder.INSTANCE,
                true);
    }

    private static class DisposeActionHolder {
        static final Action1<Reader> INSTANCE = new Action1<Reader>() {
            @Override
            public void call(Reader reader) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
    }

    public static Observable<String> join(Observable<String> source) {
        return join(source, "");
    }

    public static Observable<String> decode(Observable<byte[]> source, CharsetDecoder decoder) {
        return source.compose(Transformers.decode(decoder));
    }

    public static Observable<String> decode(Observable<byte[]> source, Charset charset) {
        return decode(source, charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.REPLACE));
    }

    public static Observable<String> decode(Observable<byte[]> source, String charset) {
        return decode(source, Charset.forName(charset));
    }

    public static Observable<String> join(final Observable<String> source, final String delimiter) {

        return Observable.defer(new Func0<Observable<String>>() {
            final AtomicBoolean afterFirst = new AtomicBoolean(false);
            final AtomicBoolean isEmpty = new AtomicBoolean(true);

            @Override
            public Observable<String> call() {
                return source.collect(new Func0<StringBuilder>() {
                    @Override
                    public StringBuilder call() {
                        return new StringBuilder();
                    }
                }, new Action2<StringBuilder, String>() {

                    @Override
                    public void call(StringBuilder b, String s) {
                        if (!afterFirst.compareAndSet(false, true)) {
                            b.append(delimiter);
                        }
                        b.append(s);
                        isEmpty.set(false);
                    }
                }).flatMap(new Func1<StringBuilder, Observable<String>>() {

                    @Override
                    public Observable<String> call(StringBuilder b) {
                        if (isEmpty.get())
                            return Observable.empty();
                        else
                            return Observable.just(b.toString());
                    }
                });

            }
        });
    }

    public static Observable<List<String>> splitLines(InputStream is, Charset charset,
            final String delimiter, final String commentPrefix) {
        return from(is, charset).compose(Transformers.split("\n")) //
                .filter(new Func1<String, Boolean>() {
                    @Override
                    public Boolean call(String line) {
                        return !line.startsWith(commentPrefix);
                    }
                }) //
                .map(SplitLinesHolder.trim) //
                .filter(SplitLinesHolder.notEmpty) //
                .map(new Func1<String, List<String>>() {
                    @Override
                    public List<String> call(String line) {
                        return Arrays.asList(line.split(delimiter));
                    }
                });
    }
    
    public static Observable<List<String>> splitLines(InputStream is, String delimiter) {
        return splitLines(is, DEFAULT_CHARSET, delimiter, "#");
    }

    private static class SplitLinesHolder {
        static final Func1<String, String> trim = new Func1<String, String>() {
            @Override
            public String call(String line) {
                return line.trim();
            }
        };
        static final Func1<String, Boolean> notEmpty = new Func1<String, Boolean>() {
            @Override
            public Boolean call(String line) {
                return !line.isEmpty();
            }
        };
    }

}