package history.loaders;

import static history.Event.EventType.READ;
import static history.Event.EventType.WRITE;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.io.LittleEndianDataInputStream;
import com.google.common.io.LittleEndianDataOutputStream;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;

import history.*;
import history.History.*;

@SuppressWarnings("UnstableApiUsage")
public class DBCopHistoryLoader implements HistoryParser<Long, Long> {
	private final File logFile;

	private static final long INIT_SESSION_ID = 0;
	private static final long INIT_TXN_ID = 0;

	public DBCopHistoryLoader(Path logPath) {
		logFile = logPath.toFile();
	}

	@Override
	@SneakyThrows
	public History<Long, Long> loadHistory() {
		var in = new LittleEndianDataInputStream(new FileInputStream(logFile));
		return (new InternalLoader(in)).load();
	}

	@Override
	@SneakyThrows
	public void dumpHistory(History<Long, Long> history) {
		var out = new LittleEndianDataOutputStream(new FileOutputStream(logFile));
		(new InternalDumper(history, out)).dump();
	}

	@RequiredArgsConstructor
	private static class InternalLoader {
		private final History<Long, Long> history = new History<>();
		private final Set<Long> keys = new HashSet<>();
		private long sessionId = 1;
		private long transactionId = 1;
		private final LittleEndianDataInputStream in;

		@SneakyThrows
		History<Long, Long> load() {
			parseHistory();

			var init = history.addTransaction(history.addSession(0), 0);
			for (var k : keys) {
				history.addEvent(init, WRITE, k, 0L);
			}

			return history;
		}

		@SneakyThrows
		private void parseHistory() {
			var id = in.readLong();
			var nodeNum = in.readLong();
			var variableNum = in.readLong();
			var transactionNum = in.readLong();
			var eventNum = in.readLong();
			var info = parseString();
			var start = parseString();
			var end = parseString();

			var length = in.readLong();
			for (long i = 0; i < length; i++) {
				parseSession();
			}
		}

		@SneakyThrows
		private String parseString() {
			var size = in.readLong();
			assert size <= Integer.MAX_VALUE;
			return new String(in.readNBytes((int) size), StandardCharsets.UTF_8);
		}

		@SneakyThrows
		void parseSession() {
			var length = in.readLong();
			var session = history.addSession(sessionId++);
			for (long i = 0; i < length; i++) {
				parseTransaction(session);
			}
		}

		@SneakyThrows
		void parseTransaction(Session<Long, Long> session) {
			var length = in.readLong();
			var events = new ArrayList<Triple<Event.EventType, Long, Long>>();
			for (long i = 0; i < length; i++) {
				var write = in.readBoolean();
				var key = in.readLong();
				var value = in.readLong();
				var success = in.readBoolean();

				if (success) {
					keys.add(key);
					events.add(Triple.of(write ? WRITE : READ, key, value));
				}
			}

			var success = in.readBoolean();
			if (success) {
				var txn = history.addTransaction(session, transactionId++);
				events.forEach(t -> history.addEvent(txn, t.getLeft(), t.getMiddle(), t.getRight()));
			}
		}
	}

	@RequiredArgsConstructor
	private static class InternalDumper {
		private final History<Long, Long> history;
		private final LittleEndianDataOutputStream out;

		@SneakyThrows
		void dump() {
			out.writeLong(0); // id
			out.writeLong(history.getSessions().size()); // nodeNum
			out.writeLong(history.getEvents().stream().map(ev -> ev.getKey()).collect(Collectors.toSet()).size()); // variableNum
			out.writeLong(history.getTransactions().size()); // transactionNum
			out.writeLong(history.getEvents().size()); // eventNum
			dumpString("generated by SIVerifier"); // info
			var d = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(new Date());
			dumpString(d); // start
			dumpString(d); // end

			out.writeLong(history.getSessions().size());
			for (var s : history.getSessions()) {
				dumpSession(s);
			}
		}

		@SneakyThrows
		void dumpSession(Session<Long, Long> session) {
			out.writeLong(session.getTransactions().size());
			for (var txn : session.getTransactions()) {
				dumpTransaction(txn);
			}
		}

		@SneakyThrows
		void dumpTransaction(Transaction<Long, Long> transaction) {
			out.writeLong(transaction.getEvents().size());
			for (var ev : transaction.getEvents()) {
				out.writeBoolean(ev.getType() == WRITE);
				out.writeLong(ev.getKey());
				out.writeLong(ev.getValue());
				out.writeBoolean(true); // success
			}

			out.writeBoolean(true); // success
		}

		@SneakyThrows
		void dumpString(String str) {
			var bytes = str.getBytes(StandardCharsets.UTF_8);
			out.writeLong(bytes.length);
			out.write(bytes);
		}
	}

	@Override
	public <T, U> History<Long, Long> convertFrom(History<T, U> history) {
		var events = history.getEvents();
		var keys = Utils.getIdMap(events.stream().map(Event::getKey), 1);
		var values = Utils.getIdMap(events.stream().map(Event::getValue), 1);

		return Utils.convertHistory(history, ev -> Pair.of(keys.get(ev.getKey()), values.get(ev.getValue())),
				ev -> true);
	}
}
