package com.example;
import static java.lang.String.join;
import static java.util.Collections.nCopies;
import static java.util.Collections.emptyList;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Stack;
import java.util.Vector;
import java.util.Arrays;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectOutput;
import java.io.ObjectInput;
import java.io.Serializable;
import java.io.Externalizable;
import java.nio.ByteBuffer;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Demo {
	private static void writeDemo(String filename, Object... objects) throws Exception {
		System.out.println("== Writing " + filename + " ==");
		try (FileOutputStream fout = new FileOutputStream(filename+".obj", false);
				ObjectOutputStream oos = new ObjectOutputStream(fout);) {
			for (Object o : objects) {
				oos.writeObject(o);
			}
		}
	}

	private static void resetDemo() throws Exception {
		try (FileOutputStream fout = new FileOutputStream("reset.obj", false);
				ObjectOutputStream oos = new ObjectOutputStream(fout);) {
			oos.writeObject("helloWorld"); // handle #1
			oos.writeObject("helloWorld"); // ref to handle #1
			oos.reset();
			oos.writeObject("foobar"); // handle #1 again
			oos.writeObject("foobar"); // ref to handle #1 shouldn't be old value
		}
	}

	private static void blockData() throws Exception {
		try (FileOutputStream fout = new FileOutputStream("block.obj", false);
				ObjectOutputStream oos = new ObjectOutputStream(fout);) {
			oos.writeInt(42);
			oos.writeLong(84); // 42 but twice as long
			oos.writeUTF("helloWorld");
		}
	}

	private static void longBlockData() throws Exception {
		try (FileOutputStream fout = new FileOutputStream("block_long.obj", false);
				ObjectOutputStream oos = new ObjectOutputStream(fout);) {
			byte[] bytes = new byte[256]; // anything over 255 is long
			oos.write(bytes);
		}
	}

	public static void main(String[] args) throws Exception {
		writeDemo("pojo", new Parent());
		writeDemo("extended_pojo", new Child(1, "helloWorld"));
		writeDemo("childArray", (Object)new Child[]{
			new Child(2, "foo"),
			new Child(3, "bar")});

		Map proxy = (Map) Proxy.newProxyInstance(
				MeaningOfLife.class.getClassLoader(),
				new Class[] { Map.class },
				new MeaningOfLife());
		writeDemo("proxy", proxy);

		// Write String - handled differently to object
		writeDemo("strings", "helloWorld", "hello", "world", "helloWorld");

		String lorem = "Lorem ipsum dolor sit amet,"
			+ "consectetur adipiscing elit,"
			+ "sed do eiusmod tempor incididunt ut"
			+ "labore et dolore magna aliqua";

		writeDemo("string_long", join("\n", nCopies(600, lorem)));

		// Write ArrayList of children - including previously seen one
		List<Child> children = new ArrayList<>();
		Child c = new Child(4, "duplicate");
		children.add(new Child(5, "foobar"));
		children.add(c);
		children.add(c);
		writeDemo("arraylist", children);

		// Write null
		writeDemo("null", (Object)null);

		// Write enum type
		writeDemo("enum", Direction.EAST);

		// Write primitive arrays
		double[] arr = new double[] {1.2, 2.3, 3.4, 4.5};
		boolean[] truth = new boolean[] {true, true, false, true};
		writeDemo("array_primitive", arr, truth);

		// Write nested primitive array
		boolean[][] nested = new boolean[][]{{true, false},{false, true}};
		writeDemo("array_nested", (Object)nested);

		// Write primitives as object - casts to Boxed versions
		writeDemo(
			"primitives",
			true,
			'A',
			(short)-42,
			-23,
			-84L,
			1.23f,
			4.56,
			(byte)76,
			Float.NaN,
			Double.NaN,
			Double.POSITIVE_INFINITY
		);

		// Write a class description without an instance
		writeDemo("class_only", Parent.class);

		// Write an object with a reference to itself
		writeDemo("loop_reference", new Loop());

		// write an object with custon writeObject method
		Custom cust = new Custom(1,2,"helloWorld");
		CustomChild custChild = new CustomChild(3, 5, "fooBar");
		writeDemo("custom_write", cust, custChild);

		// Serialize record type
		writeDemo("sample_record", new SampleRecord(42, "helloWorld"));

		// Reset and re-reread
		resetDemo();

		// send raw data
		blockData();
		longBlockData();

		Extern ext = new Extern(17);
		writeDemo("extern", ext);

		List<String> names = Arrays.asList("foo", "foo", "bar", "buzz", "fizz");
		Polymorphic arrays = new Polymorphic(names);
		Polymorphic array = new Polymorphic(new ArrayList(names));
		Polymorphic linked = new Polymorphic(new LinkedList(names));
		Stack<String> nameStack = new Stack();
		names.stream().forEach(nameStack::push);
		Polymorphic stack = new Polymorphic(nameStack);
		Polymorphic empty = new Polymorphic(emptyList());
		Polymorphic vec = new Polymorphic(new Vector<>(names));
		writeDemo("polymorphism", arrays, array, linked, stack, empty, vec);
	}

}

class Parent implements Serializable {
	int i = 13;
	String foo = "bar";
}

class Child extends Parent {
	int i = 43;
	Direction d = Direction.EAST;
	String bar = "HelloWorld";
	double[] arr = new double[] {1.2, 2.3, 3.4, 4.5};
	Other other = new Other();
	Child(int i, String bar) {
		this.i = i;
		this.bar = bar;
	}
}

class Loop implements Serializable {
	private Loop loop;
	public Loop() {
		this.loop = this;
	}
}

class Other implements Serializable {
	String abc = "def";
}

enum Direction {
	NORTH("N"), EAST("E"), SOUTH("S"), WEST("W");
	private String abbr;
	private int count;
	Direction(String abbr) {
		this.abbr = abbr;
		this.count = 0;
	}
	public void inc() {
		count += 1;
	}
}

class Custom implements Serializable {
	int i;
	transient int j;
	transient String msg;
	public Custom(int i, int j, String msg) {
		this.i = i;
		this.j = j;
		this.msg = msg;
	}
	private void writeObject(ObjectOutputStream oos) throws IOException  {
		oos.defaultWriteObject();
		oos.writeInt(j);
		oos.writeObject(msg);
		oos.writeInt(j);
	}
}

class CustomChild extends Custom {
	int i = 23;
	transient int foo = 211;
	public CustomChild(int i, int j, String msg) {
		super(i,j,msg);
		this.i = i*j;
	}
	private void writeObject(ObjectOutputStream oos) throws IOException  {
		oos.defaultWriteObject();
		oos.writeInt(foo);
	}
}

class MeaningOfLife implements InvocationHandler, Serializable {
	private Integer foo;
	public Object invoke(Object proxy, Method method, Object[] args) {
		// System.out.println("Proxy returned: " + 42);
		return 42;
	}
}

class Extern extends Parent implements Externalizable {
	private static final long serialVersionUID = 1234L;
	private int ext;
	public Extern(int i) {
		this.ext = i;
	}
	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("writing extern");
		out.writeInt(ext);
	}

	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		byte[] bits = new byte[4];
		in.read(bits);
		this.ext = ByteBuffer.wrap(bits).getInt();
	}
}

class Polymorphic implements Serializable {
	private List<String> names;
	public Polymorphic(List<String> names) {
		this.names = names;
	}
}

record SampleRecord(int i, String s) implements Serializable {}
