import java.io.*;
import java.nio.charset.*;
import java.util.*;

public class IoStreamsDemo {
    private static final Charset UTF8 = StandardCharsets.UTF_8;

    // Simulate a transport that delivers at most one byte per read.
    static final class OneByteInput extends ByteArrayInputStream {
        OneByteInput(byte[] bytes) { super(bytes); }
        @Override public synchronized int read(byte[] b, int off, int len) {
            return super.read(b, off, Math.min(len, 1));
        }
    }

    static String readText(byte[] bytes) throws IOException {
        var decoder = UTF8.newDecoder()
                .onMalformedInput(CodingErrorAction.REPORT)
                .onUnmappableCharacter(CodingErrorAction.REPORT);
        try (var reader = new BufferedReader(
                new InputStreamReader(new OneByteInput(bytes), decoder))) {
            var text = new StringBuilder();
            char[] buffer = new char[2];
            for (int n; (n = reader.read(buffer)) != -1;) {
                text.append(buffer, 0, n);
            }
            return text.toString();
        }
    }

    static String units(String text) {
        return Arrays.toString(text.chars()
                .mapToObj(c -> String.format("U+%04X", c)).toArray());
    }

    static final class Resource implements AutoCloseable {
        private final String name;
        Resource(String name) { this.name = name; }
        @Override public void close() throws IOException {
            System.out.println("close " + name);
            throw new IOException("close " + name);
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length > 0 && args[0].equals("truncated")) {
            // Expected failure: EOF after only two bytes of a three-byte sequence.
            readText(new byte[] {(byte) 0xE4, (byte) 0xB8});
            throw new AssertionError("truncated UTF-8 was accepted");
        }
        byte[] bytes = "A\u4E2DB".getBytes(UTF8);
        String wrong = new String(bytes, 0, 2, UTF8)
                + new String(bytes, 2, bytes.length - 2, UTF8);
        System.out.println("per-chunk=" + units(wrong));
        if (wrong.equals("A\u4E2DB")) throw new AssertionError("bad example");
        String correct = readText(bytes);
        System.out.println("stream=" + units(correct));
        if (!correct.equals("A\u4E2DB")) throw new AssertionError("decode");
        String emoji = readText("\uD83D\uDE00".getBytes(UTF8));
        System.out.println("emoji char-units=" + emoji.length()
                + ", code-points=" + emoji.codePointCount(0, emoji.length()));

        var sink = new ByteArrayOutputStream();
        try (var writer = new BufferedWriter(new OutputStreamWriter(sink, UTF8))) {
            writer.write("A\u4E2DB");
            System.out.println("before flush=" + sink.size());
            if (sink.size() != 0) throw new AssertionError("buffer");
            writer.flush();
            System.out.println("after flush=" + sink.size());
            if (sink.size() != 5) throw new AssertionError("flush");
        }
        try (var first = new Resource("first"); var second = new Resource("second")) {
            throw new IOException("body");
        } catch (IOException e) {
            System.out.println("primary=" + e.getMessage());
            System.out.println("suppressed=" + Arrays.toString(Arrays.stream(
                    e.getSuppressed()).map(Throwable::getMessage).toArray()));
            if (!e.getMessage().equals("body") || e.getSuppressed().length != 2)
                throw new AssertionError("suppression", e);
        }
    }
}
