/*
 * Copyright (C) 2009.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */
package uk.me.parabola.imgfmt.app.net;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import it.unimi.dsi.fastutil.ints.IntArrayList;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BufferedImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFile;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.City;
import uk.me.parabola.imgfmt.app.lbl.LBLFileReader;
import uk.me.parabola.imgfmt.app.lbl.Zip;
import uk.me.parabola.imgfmt.fs.ImgChannel;

/**
 * Read the NET file.
 */
public class NETFileReader extends ImgFile {
	private final NETHeader netHeader = new NETHeader();

	// To begin with we only need LBL offsets.
	private final Map<Integer, Integer> offsetLabelMap = new HashMap<>();
	private List<Integer> offsets;

	private List<City> cities;
	private int citySize;

	private List<Zip> zips;
	private int zipSize;
	private LBLFileReader labels;

	public NETFileReader(ImgChannel chan) {
		setHeader(netHeader);

		setReader(new BufferedImgFileReader(chan));
		netHeader.readHeader(getReader());

		readLabelOffsets();
	}

	/**
	 * Get the label offset, given the NET offset.
	 * @param netOffset An offset into NET 1, as found in the road entries in
	 * RGN for example.
	 * @return The offset into LBL as found in NET 1.
	 */
	public int getLabelOffset(int netOffset) {
		return offsetLabelMap.getOrDefault(netOffset, 0);
	}

	/**
	 * Get the list of roads from the net section.
	 *
	 * Saving the bare minimum that is needed, please improve.
	 * @return A list of RoadDefs. Note that currently not everything is
	 * populated in the road def so it can't be written out as is.
	 */
	public List<RoadDef> getRoads() {
		ImgFileReader reader = getReader();
		long start = netHeader.getRoadDefinitionsStart();

		List<RoadDef> roads = new ArrayList<>();
		int record = 0;
		for (int off : offsets) {
			reader.position(start + off);

			RoadDef road = new RoadDef(++record, off, null);
			readLabels(reader, road);
			byte netFlags = reader.get();
			/*int len =*/ reader.get3u();

			int[] counts = new int[24];
			int level = 0;
			while (level < 24) {
				int n = reader.get();
				counts[level++] = n & 0x7f;
				if ((n & 0x80) != 0)
					break;
			}

			for (int i = 0; i < level; i++) {
				int c = counts[i];
				for (int j = 0; j < c; j++) {
					/*byte b =*/ reader.get();
					/*char sub =*/ reader.get2u();
				}
			}

			if ((netFlags & RoadDef.NET_FLAG_ADDRINFO) != 0) {
				int flags2 = reader.get2u();

				int zipFlag = (flags2 >> 10) & 0x3;
				int cityFlag = (flags2 >> 12) & 0x3;
				int numberFlag = (flags2 >> 14) & 0x3;
				IntArrayList indexes = new IntArrayList();
				fetchZipCityIndexes(reader, zipFlag, zipSize, indexes);
				for (int index : indexes){
					road.addZipIfNotPresent(zips.get(index));
				}
				fetchZipCityIndexes(reader, cityFlag, citySize, indexes);
				for (int index : indexes){
					road.addCityIfNotPresent(cities.get(index));
				}

				fetchNumber(reader, numberFlag);
			}

			if ((netFlags & RoadDef.NET_FLAG_NODINFO) != 0) {
				int nodFlags = reader.get1u();
				int nbytes = nodFlags & 0x3;
				if (nbytes > 0) {
					/*int nod = */reader.getNu(nbytes+1);
				}
			}

			roads.add(road);
		}
		return roads;
	}

	/**
	 * Parse a list of zip/city indexes.
	 * @param reader
	 * @param flag
	 * @param size
	 * @param indexes
	 */
	private static void fetchZipCityIndexes(ImgFileReader reader, int flag, int size, IntArrayList indexes) {
		indexes.clear();
		if (flag == 2) {
			// fetch city/zip index
			int ind = reader.getNu(size);
			if (ind != 0)
				indexes.add(ind-1);
		} else if (flag == 3) {
			// there is no item
		} else if (flag == 0) {
			int n = reader.get1u();
			parseList(reader, n, size, indexes);
		} else if (flag == 1) {
			int n = reader.get2u();
			parseList(reader, n, size, indexes);
		} else {
			assert false : "flag is " + flag;
		}
	}
	
	private static void parseList(ImgFileReader reader, int n, int size,
			IntArrayList indexes) {
		long endPos = reader.position() + n;
		int node = 0; // not yet used
		while (reader.position() < endPos) {
			int initFlag = reader.get1u();
			int skip = initFlag & 0x1f;
			initFlag >>= 5;
			if (initFlag == 7) {
				// Need to read another byte
				initFlag = reader.get1u();
				skip |= (initFlag & 0x1f) << 5;
				initFlag >>= 5;
			}
			node += skip + 1;
			int right = 0;
			int left = 0;
			if (initFlag == 0) {
				right = left = getCityOrZip(reader, size, endPos);
			} else if ((initFlag & 0x4) != 0) {
				if ((initFlag & 1) == 0)
					right = 0;
				if ((initFlag & 2) == 0)
					left = 0;
			} else {
				if ((initFlag & 1) != 0)
					left = getCityOrZip(reader, size, endPos);
				if ((initFlag & 2) != 0)
					right = getCityOrZip(reader, size, endPos);
			}
			if (left > 0)
				indexes.add(left - 1);
			if (right > 0 && left != right)
				indexes.add(right - 1);
		}
	}

	private static int getCityOrZip(ImgFileReader reader, int size, long endPos) {
		if (reader.position() > endPos - size) {
			assert false : "ERRROR overflow";
			return 0;
		}
		return reader.getNu(size);
	}

	/**
	 * Fetch a block of numbers.
	 * @param reader The reader.
	 * @param numberFlag The flag that says how the block is formatted.
	 */
	private static void fetchNumber(ImgFileReader reader, int numberFlag) {
		int n = 0;
		if (numberFlag == 0) {
			n = reader.get1u();
		} else if (numberFlag == 1) {
			n = reader.get2u();
		} else if (numberFlag == 3) {
			// There is no block
			return;
		} else {
			// Possible but don't know what to do in this context
			assert false;
		}
		if (n > 0)
			reader.get(n);
	}

	private void readLabels(ImgFileReader reader, RoadDef road) {
		for (int i = 0; i < 4; i++) {
			int lab = reader.get3u();
			Label label = labels.fetchLabel(lab & 0x7fffff);
			road.addLabel(label);
			if ((lab & 0x800000) != 0)
				break;
		}
	}

	/**
	 * The first field in NET 1 is a label offset in LBL.  Currently we
	 * are only interested in that to convert between a NET 1 offset and
	 * a LBL offset.
	 */
	private  void readLabelOffsets() {
		ImgFileReader reader = getReader();
		offsets = readOffsets();
		long start = netHeader.getRoadDefinitionsStart();
		for (int off : offsets) {
			reader.position(start + off);
			int labelOffset = reader.get3u();
			offsetLabelMap.put(off, labelOffset & 0x7fffff);
		}
	}

	/**
	 * NET 3 contains a list of all the NET 1 record start positions.  They
	 * are in alphabetical order of name.  So read them in and sort into
	 * memory address order.
	 * @return A list of start offsets in NET 1, sorted by increasing offset.
	 */
	private List<Integer> readOffsets() {
		int start = netHeader.getSortedRoadsStart();
		int end = netHeader.getSortedRoadsEnd();
		ImgFileReader reader = getReader();
		reader.position(start);

		Set<Integer> allOffsets = new HashSet<>();
		while (reader.position() < end) {
			int net1 = reader.get3u();

			// The offset is stored in the bottom 22 bits. The top 2 bits are an index into the list
			// of lbl pointers in the net1 entry. 
			allOffsets.add((net1 & 0x3fffff) << netHeader.getRoadShift());
		}

		// Sort in address order in the hope of speeding up reading.
		List<Integer> sortedOffsets = new ArrayList<>(allOffsets);
		sortedOffsets.sort(null);
		return sortedOffsets;
	}

	public void setCities(List<City> cities) {
		this.cities = cities;
		this.citySize = Utils.numberToPointerSize(cities.size());
	}

	public void setZips(List<Zip> zips) {
		this.zips = zips;
		this.zipSize = Utils.numberToPointerSize(zips.size());
	}

	public void setLabels(LBLFileReader labels) {
		this.labels = labels;
	}
}
