package org.eclipse.equinox.metatype.impl;

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

/*******************************************************************************
 * Copyright (c) 2019 IBM Corporation.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

public class Persistence {
	private static final int PERSISTENCE_VERSION = 0;
	private static final byte NULL = 0;
	private static final byte OBJECT = 1;
	private static final byte INDEX = 2;
	private static final byte LONG_STRING = 3;

	private Persistence() {
	}

	public static class Reader implements Closeable {
		private final Map<Integer, String> readStringCache = new HashMap<>();
		private final DataInputStream in;

		public Reader(DataInputStream in) {
			this.in = in;
		}

		public boolean isValidPersistenceVersion() throws IOException {
			return in.readInt() == PERSISTENCE_VERSION;
		}

		public void readIndexedStrings() throws IOException {
			int num = in.readInt();
			for (int i = 0; i < num; i++) {
				readIndexedString();
			}
		}

		private String readIndexedString() throws IOException {
			String string = readString();
			readStringCache.put(Integer.valueOf(in.readInt()), string);
			return string;
		}

		public String readString() throws IOException {
			byte type = in.readByte();
			if (type == INDEX) {
				int index = in.readInt();
				return readStringCache.get(index);
			}
			if (type == NULL) {
				return null;
			}
			String string;
			if (type == LONG_STRING) {
				int length = in.readInt();
				byte[] data = new byte[length];
				in.readFully(data);
				string = new String(data, StandardCharsets.UTF_8);
			} else {
				string = in.readUTF();
			}

			return string;
		}

		public long readLong() throws IOException {
			return in.readLong();
		}

		public int readInt() throws IOException {
			return in.readInt();
		}

		@Override
		public void close() throws IOException {
			in.close();
		}

		public boolean readBoolean() throws IOException {
			return in.readBoolean();
		}

		public short readShort() throws IOException {
			return in.readShort();
		}

		public char readCharacter() throws IOException {
			return in.readChar();
		}

		public byte readByte() throws IOException {
			return in.readByte();
		}

		public double readDouble() throws IOException {
			return in.readDouble();
		}

		public float readFloat() throws IOException {
			return in.readFloat();
		}
	}

	public static class Writer implements Closeable {
		private final Map<String, Integer> writeStringCache = new HashMap<>();
		private final DataOutputStream out;

		public Writer(DataOutputStream out) {
			this.out = out;
		}

		public void writePersistenceVersion() throws IOException {
			out.writeInt(PERSISTENCE_VERSION);
		}

		public void writeIndexedStrings(Set<String> strings) throws IOException {
			strings.remove(null); // do not index null
			out.writeInt(strings.size());
			for (String string : strings) {
				writeIndexedString(string);
			}
		}

		private void writeIndexedString(String string) throws IOException {
			writeString(string);
			addToIndex(string);
		}

		public void writeString(String string) throws IOException {
			Integer index = string != null ? writeStringCache.get(string) : null;
			if (index != null) {
				out.writeByte(INDEX);
				out.writeInt(index);
				return;
			}

			if (string == null)
				out.writeByte(NULL);
			else {
				byte[] data = string.getBytes(StandardCharsets.UTF_8);

				if (data.length > 65535) {
					out.writeByte(LONG_STRING);
					out.writeInt(data.length);
					out.write(data);
				} else {
					out.writeByte(OBJECT);
					out.writeUTF(string);
				}
			}
		}

		private void addToIndex(String string) throws IOException {
			if (string == null) {
				throw new NullPointerException();
			}
			Integer cur = writeStringCache.get(string);
			if (cur != null)
				throw new IllegalStateException("String is already in the write table: " + string); //$NON-NLS-1$
			Integer index = writeStringCache.size();
			writeStringCache.put(string, index);
			out.writeInt(index.intValue());
		}

		@Override
		public void close() throws IOException {
			out.close();
		}

		public void writeInt(int v) throws IOException {
			out.writeInt(v);
		}

		public void writeLong(long v) throws IOException {
			out.writeLong(v);
		}

		public void writeBoolean(boolean v) throws IOException {
			out.writeBoolean(v);
		}

		public void writeShort(Short v) throws IOException {
			out.writeShort(v);
		}

		public void writeCharacter(Character v) throws IOException {
			out.writeChar(v);
		}

		public void writeByte(Byte v) throws IOException {
			out.writeByte(v);
		}

		public void writeDouble(Double v) throws IOException {
			out.writeDouble(v);
		}

		public void writeFloat(Float v) throws IOException {
			out.writeFloat(v);
		}
	}
}
