package org.kareha.hareka.server.game;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;

import org.kareha.hareka.annotation.GuardedBy;
import org.kareha.hareka.annotation.Private;
import org.kareha.hareka.field.Direction;
import org.kareha.hareka.field.FieldObject;
import org.kareha.hareka.field.Placement;
import org.kareha.hareka.field.TileType;
import org.kareha.hareka.field.Transformation;
import org.kareha.hareka.field.Vector;
import org.kareha.hareka.field.Vectors;
import org.kareha.hareka.server.game.entity.CharacterEntity;
import org.kareha.hareka.server.game.entity.FieldEntity;
import org.kareha.hareka.server.game.field.ServerField;
import org.kareha.hareka.server.game.field.ServerFieldObject;
import org.kareha.hareka.server.game.stat.NormalCharacterStat;
import org.kareha.hareka.server.game.stat.Species;
import org.kareha.hareka.util.JaxbUtil;

public class CharacterSpawner {

	private static final String resourceFile = "org/kareha/hareka/server/game/resource/CharacterSpawner.xml";

	private final File file;
	@GuardedBy("this")
	@Private
	final Map<TileType, Map<String, Float>> map = new EnumMap<>(TileType.class);
	private volatile Map<TileType, List<RateEntry>> table;

	public CharacterSpawner(final File file) throws JAXBException {
		this.file = file;
		final Adapted adapted;
		if (file.isFile()) {
			adapted = JaxbUtil.unmarshal(file, Adapted.class);
		} else {
			try (final InputStream in = getClass().getClassLoader().getResourceAsStream(resourceFile)) {
				adapted = JaxbUtil.unmarshal(in, JAXBContext.newInstance(Adapted.class));
			} catch (final IOException e) {
				throw new JAXBException(e);
			}
		}
		if (adapted != null && adapted.rates != null) {
			for (final Rates rates : adapted.rates) {
				final Map<String, Float> table = new HashMap<>();
				for (final Rate rate : rates.rate) {
					table.put(rate.species, rate.value);
				}
				map.put(rates.tileType, table);
			}
		}
		update();
	}

	@XmlType(name = "rate")
	@XmlAccessorType(XmlAccessType.NONE)
	private static class Rate {

		@XmlAttribute
		@Private
		String species;
		@XmlValue
		@Private
		float value;

		@SuppressWarnings("unused")
		private Rate() {
			// used by JAXB
		}

		Rate(final String id, final float value) {
			this.species = id;
			this.value = value;
		}

	}

	@XmlType(name = "rates")
	@XmlAccessorType(XmlAccessType.NONE)
	private static class Rates {

		@XmlAttribute(name = "tile")
		@Private
		TileType tileType;
		@XmlElement
		@Private
		List<Rate> rate;

		@SuppressWarnings("unused")
		private Rates() {
			// used by JAXB
		}

		Rates(final TileType tileType, final Map<String, Float> table) {
			this.tileType = tileType;
			rate = new ArrayList<>();
			for (final Map.Entry<String, Float> entry : table.entrySet()) {
				rate.add(new Rate(entry.getKey(), entry.getValue()));
			}
		}

	}

	@XmlRootElement(name = "characterSpawner")
	@XmlAccessorType(XmlAccessType.NONE)
	private static class Adapted {

		@XmlElement
		@Private
		List<Rates> rates;

		@SuppressWarnings("unused")
		private Adapted() {
			// used by JAXB
		}

		@Private
		Adapted(final CharacterSpawner v) {
			rates = new ArrayList<>();
			synchronized (v) {
				for (final Map.Entry<TileType, Map<String, Float>> entry : v.map.entrySet()) {
					rates.add(new Rates(entry.getKey(), entry.getValue()));
				}
			}
		}

	}

	public void save() throws JAXBException {
		JaxbUtil.marshal(new Adapted(this), file);
	}

	private static class RateEntry {

		final String species;
		final float value;

		RateEntry(final String species, final float value) {
			this.species = species;
			this.value = value;
		}

	}

	private void update() {
		final Map<TileType, List<RateEntry>> newTable = new EnumMap<>(TileType.class);
		for (final TileType tileType : TileType.values()) {
			final Map<String, Float> rateMap = map.get(tileType);
			if (rateMap == null) {
				continue;
			}
			float total = 0;
			for (final Float value : rateMap.values()) {
				total += value;
			}
			final List<RateEntry> list = new ArrayList<>();
			float current = 0;
			for (final Map.Entry<String, Float> entry : rateMap.entrySet()) {
				current += entry.getValue() / total;
				list.add(new RateEntry(entry.getKey(), current));
			}
			if (!list.isEmpty()) {
				final RateEntry last = list.remove(list.size() - 1);
				list.add(new RateEntry(last.species, 1));
			}
			newTable.put(tileType, list);
		}
		table = newTable;
	}

	private String findSpecies(final TileType tileType, final float randomValue) {
		final List<RateEntry> list = table.get(tileType);
		if (list == null) {
			return null;
		}
		for (final RateEntry rateEntry : list) {
			if (rateEntry.value > randomValue) {
				return rateEntry.species;
			}
		}
		return null;
	}

	public boolean trySpawnCharacter(final ServerFieldObject fo, final Direction newDirection,
			final Vector newPosition) {
		if (newDirection == Direction.NULL) {
			return false;
		}
		if (ThreadLocalRandom.current().nextInt(8) != 0) {
			return false;
		}
		final ServerField field = fo.getField();
		final Vector[] edge = Vectors.edge(newPosition, field.getViewSize() + 1, newDirection);
		final int i = ThreadLocalRandom.current().nextInt(edge.length);
		final Vector center = edge[i];
		final TileType tileType = field.getTile(center).type();
		if (!tileType.isWalkable()) {
			return false;
		}
		if (tileType == TileType.AIR) {
			return false;
		}
		final List<FieldObject> fos = field.getInViewObjects(center);
		int aliveCount = 0;
		int deadCount = 0;
		int spiritCount = 0;
		for (final FieldObject j : fos) {
			final FieldEntity fe = ((ServerFieldObject) j).getFieldEntity();
			if (!(fe instanceof CharacterEntity)) {
				continue;
			}
			final CharacterEntity ce = (CharacterEntity) fe;
			if (!ce.getStat().getHealthPoints().isAlive()) {
				deadCount++;
				continue;
			}
			if (!ce.isFieldDriversEmpty()) {
				return false;
			}
			if (ce.getStat().getSpecies().getId().equals("Spirit")) {
				spiritCount++;
				continue;
			}
			aliveCount++;
		}

		// create character

		String raceId = null;
		if (deadCount > 0 && spiritCount < 4) {
			if (ThreadLocalRandom.current().nextInt(8) < 1) {
				raceId = "Spirit";
			}
		}
		if (raceId == null) {
			raceId = findSpecies(tileType, ThreadLocalRandom.current().nextFloat());
		}
		if (raceId == null) {
			return false;
		}
		if (!raceId.equals("Spirit") && aliveCount >= 8) {
			return false;
		}
		final Species race = Global.INSTANCE.context().getSpeciesTable().getSpecies(raceId);
		final List<String> shapes = new ArrayList<>(race.getShapes());
		final String shape = shapes.get(ThreadLocalRandom.current().nextInt(shapes.size()));

		final CharacterEntity.Builder builder = new CharacterEntity.Builder();
		builder.name(race.getName());
		builder.shape(shape);
		final NormalCharacterStat.Builder statBuilder = new NormalCharacterStat.Builder();
		statBuilder.species(race).shape(shape);
		builder.stat(statBuilder.build());
		builder.fieldId(field.getId());
		builder.placement(Placement.valueOf(center, Direction.NULL));
		builder.transformation(Transformation.random(Vector.ZERO));
		final CharacterEntity entity = Global.INSTANCE.context().getEntities().createCharacterEntity(builder);
		entity.mount(Global.INSTANCE.context().getFields());
		entity.startMotion();

		return true;
	}

}
