package com.github.adamldavis;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.DirectProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxProcessor;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;

/**
 * Demonstrates Reactor in action.
 * 
 * @author Adam L. Davis
 */
public class ReactorDemo implements ReactiveStreamsDemo {

    public static Flux<Long> exampleSquaresUsingGenerate() {
        Flux<Long> squares = Flux.generate(
                AtomicLong::new, //1
                (state, sink) -> {
                    long i = state.getAndIncrement();
                    sink.next(i * i); //2
                    if (i == 10) sink.complete(); //3
                    return state;
                });
        return squares;
    }

    public static List<Integer> doSquares() {
        List<Integer> squares = new ArrayList<>();
        Flux.range(1, 64) // 1
                .onBackpressureBuffer(256).map(v -> v * v) // 3
                .subscribeOn(Schedulers.immediate())
                .subscribe(squares::add); // 4

        return squares;
    }

    public static List<Integer> doParallelSquares() {
        return Flux.range(1, 64).flatMap(v -> // 1
            Mono.just(v).subscribeOn(Schedulers.parallel()).map(w -> w * w))
                .doOnError(ex -> ex.printStackTrace()) // 2
                .doOnComplete(() -> System.out.println("Completed")) // 3
                .subscribeOn(Schedulers.immediate()).collectList().block();
    }

    @Override
    public Future<List<Integer>> doSquaresAsync(int count) {
        return Flux.range(1, count).map(w -> w * w)
                .subscribeOn(Schedulers.immediate()).collectList().toFuture();
    }

    @Override
    public Future<String> doStringConcatAsync(int count) {
        return Flux.range(0, count)
                .map(i -> "i=" + i)
                .collect(() -> new StringBuilder(),
                    (stringBuilder, o) -> stringBuilder.append(o))
                .map(StringBuilder::toString)
                .toFuture();
    }

    @Override
    public Future<List<Integer>> doParallelSquaresAsync(int count) {
        return Flux.range(1, count).flatMap(v -> // 1
            Mono.just(v).subscribeOn(Schedulers.parallel()).map(w -> w * w))
                .subscribeOn(Schedulers.immediate()).collectList().toFuture();
    }

    @Override
    public Future<String> doParallelStringConcatAsync(int count) {
        BiConsumer<StringBuilder, Object> collector =
                (stringBuilder, o) -> stringBuilder.append(o);
        return Flux.range(0, count)
                .map(i -> "i=" + i)
                .window(10)
                .flatMap(flux -> flux.subscribeOn(Schedulers.parallel())
                        .collect(() -> new StringBuilder(), collector))
                .collect(() -> new StringBuilder(), collector)
                .map(StringBuilder::toString)
                .single().toFuture();
    }

    public static void runComputation() throws Exception {
        StringBuffer sb = new StringBuffer();
        Mono<String> source = Mono.fromCallable(() -> { // 1
            Thread.sleep(1000); // imitate expensive computation
            return "Done";
        });
        source.doOnNext((x) -> System.out.println("Completed runComputation"));

        Flux<String> background = source.subscribeOn(Schedulers.elastic())
                .flux(); // 2

        Flux<String> foreground = background.publishOn(Schedulers.immediate());

        foreground.subscribe(System.out::println, Throwable::printStackTrace);// 4
    }

    public static void writeFile(File file) {
        try (PrintWriter pw = new PrintWriter(file)) {
            Flux.range(1, 100)
                    .publishOn(Schedulers
                            .fromExecutor(Executors.newFixedThreadPool(1)))
                    .subscribeOn(Schedulers.immediate()).subscribe(pw::println);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void readFile(File file) {
        try (final BufferedReader br = new BufferedReader(
                new FileReader(file))) {

            Flux<String> flow = Flux.from(new FilePublisher(br));

            flow.publishOn(Schedulers.elastic())
                    .subscribeOn(Schedulers.immediate())
                    .subscribe(System.out::println);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void useProcessor() {
        // Processor implements both Publisher and Subscriber
        // DirectProcessor is the simplest Processor from Reactor
        final FluxProcessor<String, String> processor = DirectProcessor.create();
        //TODO
    }

    static class FilePublisher implements Publisher<String> {
        BufferedReader reader;

        public FilePublisher(BufferedReader reader) {
            this.reader = reader;
        }

        @Override
        public void subscribe(Subscriber<? super String> subscriber) {
            subscriber.onSubscribe(
                    new FilePublisherSubscription(this, subscriber));
        }

        public String readLine() throws IOException {
            return reader.readLine();
        }
    }

    static class FilePublisherSubscription implements Subscription {
        FilePublisher publisher;
        Subscriber<? super String> subscriber;

        public FilePublisherSubscription(FilePublisher publisher,
                Subscriber<? super String> subscriber) {
            this.publisher = publisher;
            this.subscriber = subscriber;
        }

        @Override
        public void request(long n) {
            try {
                String line;
                for (int i = 0; i < n && publisher != null
                        && (line = publisher.readLine()) != null; i++) {
                    if (subscriber != null)
                        subscriber.onNext(line);
                }
            } catch (IOException ex) {
                subscriber.onError(ex);
            }
            subscriber.onComplete();
        }

        @Override
        public void cancel() {
            publisher = null;
        }
    }

}