package com.aionemu.gameserver.world.zone;

import java.util.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.aionemu.commons.scripting.ScriptManager;
import com.aionemu.commons.scripting.classlistener.AggregatedClassListener;
import com.aionemu.commons.scripting.classlistener.OnClassLoadUnloadListener;
import com.aionemu.commons.scripting.classlistener.ScheduledTaskClassListener;
import com.aionemu.gameserver.configs.main.GeoDataConfig;
import com.aionemu.gameserver.configs.main.WorldConfig;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.dataholders.ZoneData;
import com.aionemu.gameserver.geoEngine.scene.Spatial;
import com.aionemu.gameserver.model.GameEngine;
import com.aionemu.gameserver.model.geometry.*;
import com.aionemu.gameserver.model.siege.SiegeLocation;
import com.aionemu.gameserver.model.siege.SiegeShield;
import com.aionemu.gameserver.model.templates.materials.MaterialTemplate;
import com.aionemu.gameserver.model.templates.world.WorldMapTemplate;
import com.aionemu.gameserver.model.templates.zone.MaterialZoneTemplate;
import com.aionemu.gameserver.model.templates.zone.WorldZoneTemplate;
import com.aionemu.gameserver.model.templates.zone.ZoneInfo;
import com.aionemu.gameserver.model.templates.zone.ZoneTemplate;
import com.aionemu.gameserver.model.vortex.VortexLocation;
import com.aionemu.gameserver.services.ShieldService;
import com.aionemu.gameserver.world.zone.handler.*;

/**
 * @author ATracer, antness
 */
public final class ZoneService implements GameEngine {

	private static final Logger log = LoggerFactory.getLogger(ZoneService.class);
	private final Map<ZoneName, Class<? extends ZoneHandler>> zoneHandlers = new HashMap<>();
	private final Map<ZoneName, ZoneHandler> collidableHandlers = new HashMap<>();
	private final Map<Integer, List<ZoneInfo>> zoneByMapIdMap = DataManager.ZONE_DATA.getZones();

	private ZoneService() {
	}

	@Override
	public void init() {
		ScriptManager scriptManager = new ScriptManager();
		AggregatedClassListener acl = new AggregatedClassListener();
		acl.addClassListener(new OnClassLoadUnloadListener());
		acl.addClassListener(new ScheduledTaskClassListener());
		acl.addClassListener(new ZoneHandlerClassListener());
		scriptManager.setGlobalClassListener(acl);
		scriptManager.load(WorldConfig.ZONE_HANDLER_DIRECTORY);
		log.info("Loaded " + zoneHandlers.size() + " zone handlers.");
	}

	public ZoneHandler getNewZoneHandler(ZoneName zoneName) {
		ZoneHandler zoneHandler = collidableHandlers.get(zoneName);
		if (zoneHandler != null)
			return zoneHandler;
		Class<? extends ZoneHandler> zoneClass = zoneHandlers.get(zoneName);
		if (zoneClass != null) {
			try {
				zoneHandler = zoneClass.getDeclaredConstructor().newInstance();
			} catch (Exception ex) {
				log.warn("Can't instantiate zone handler " + zoneName, ex);
			}
		}

		return zoneHandler != null ? zoneHandler : new GeneralZoneHandler();
	}

	public final void addZoneHandlerClass(Class<? extends ZoneHandler> handler) {
		ZoneNameAnnotation idAnnotation = handler.getAnnotation(ZoneNameAnnotation.class);
		if (idAnnotation != null) {
			String[] zoneNames = idAnnotation.value().split(" ");
			for (String zoneNameString : zoneNames) {
				try {
					ZoneName zoneName = ZoneName.get(zoneNameString.trim());
					if (zoneName == ZoneName.get("NONE"))
						throw new RuntimeException();
					zoneHandlers.put(zoneName, handler);
				} catch (Exception e) {
					log.warn("Missing ZoneName: " + idAnnotation.value());
				}
			}
		}
	}

	public final void addZoneHandlerClass(ZoneName zoneName, Class<? extends ZoneHandler> handler) {
		zoneHandlers.put(zoneName, handler);
	}

	public Map<ZoneName, ZoneInstance> getZoneInstancesByWorldId(int mapId) {
		Map<ZoneName, ZoneInstance> zones = new HashMap<>();
		int worldSize = DataManager.WORLD_MAPS_DATA.getTemplate(mapId).getWorldSize();
		WorldZoneTemplate zone = new WorldZoneTemplate(worldSize, mapId);
		PolyArea fullArea = new PolyArea(zone.getName(), mapId, zone.getPoints().getPoint(), zone.getPoints().getBottom(), zone.getPoints().getTop());
		ZoneInstance fullMap = new ZoneInstance(mapId, new ZoneInfo(fullArea, zone));
		fullMap.addHandler(getNewZoneHandler(zone.getName()));
		zones.put(zone.getName(), fullMap);

		Collection<ZoneInfo> areas = this.zoneByMapIdMap.get(mapId);
		if (areas == null)
			return zones;

		for (ZoneInfo area : areas) {
			ZoneInstance instance;
			switch (area.getZoneTemplate().getZoneType()) {
				case FLY:
					instance = new FlyZoneInstance(mapId, area);
					break;
				case NO_FLY:
					instance = new NoFlyZoneInstance(mapId, area);
					break;
				case FORT:
					instance = new SiegeZoneInstance(mapId, area);
					SiegeLocation siege = DataManager.SIEGE_LOCATION_DATA.getSiegeLocations().get(area.getZoneTemplate().getSiegeId().get(0));
					if (siege != null) {
						siege.addZone((SiegeZoneInstance) instance);
						if (GeoDataConfig.GEO_SHIELDS_ENABLE)
							ShieldService.getInstance().attachShield(siege);
					}
					break;
				case ARTIFACT:
					instance = new SiegeZoneInstance(mapId, area);
					for (int artifactId : area.getZoneTemplate().getSiegeId()) {
						SiegeLocation artifact = DataManager.SIEGE_LOCATION_DATA.getArtifacts().get(artifactId);
						if (artifact == null) {
							log.warn("Missing siege location data for zone " + area.getZoneTemplate().getName().name());
						} else {
							artifact.addZone((SiegeZoneInstance) instance);
						}
					}
					break;
				case PVP:
					instance = new PvPZoneInstance(mapId, area);
					break;
				default:
					InvasionZoneInstance invasionZone = getIZI(area);
					if (invasionZone != null) {
						instance = invasionZone;
					} else {
						instance = new ZoneInstance(mapId, area);
					}
			}
			instance.addHandler(getNewZoneHandler(area.getZoneTemplate().getName()));
			zones.put(area.getZoneTemplate().getName(), instance);
		}
		return zones;
	}

	private InvasionZoneInstance getIZI(ZoneInfo area) {
		if (area.getZoneTemplate().getName().name().equals("WAILING_CLIFFS_220050000")
			|| area.getZoneTemplate().getName().name().equals("BALTASAR_CEMETERY_220050000")
			|| area.getZoneTemplate().getName().name().equals("THE_LEGEND_SHRINE_220050000")
			|| area.getZoneTemplate().getName().name().equals("SUDORVILLE_220050000")
			|| area.getZoneTemplate().getName().name().equals("BALTASAR_HILL_VILLAGE_220050000")
			|| area.getZoneTemplate().getName().name().equals("BRUSTHONIN_MITHRIL_MINE_220050000")) {
			return validateZone(area);
		} else if (area.getZoneTemplate().getName().name().equals("JAMANOK_INN_210060000")
			|| area.getZoneTemplate().getName().name().equals("THE_STALKING_GROUNDS_210060000")
			|| area.getZoneTemplate().getName().name().equals("BLACK_ROCK_HOT_SPRING_210060000")
			|| area.getZoneTemplate().getName().name().equals("FREGIONS_FLAME_210060000")) {
			return validateZone(area);
		}
		return null;
	}

	private InvasionZoneInstance validateZone(ZoneInfo area) {
		int mapId = area.getZoneTemplate().getMapid();
		VortexLocation vortex = DataManager.VORTEX_DATA.getVortexLocation(mapId);
		if (vortex != null) {
			InvasionZoneInstance instance = new InvasionZoneInstance(mapId, area);
			vortex.addZone(instance);
			return instance;
		}
		return null;
	}

	public void saveMaterialZones() {
		List<ZoneTemplate> templates = new ArrayList<>();
		for (WorldMapTemplate map : DataManager.WORLD_MAPS_DATA) {
			Collection<ZoneInfo> areas = this.zoneByMapIdMap.get(map.getMapId());
			if (areas == null)
				continue;
			for (ZoneInfo zone : areas) {
				if (collidableHandlers.containsKey(zone.getArea().getZoneName())) {
					templates.add(zone.getZoneTemplate());
				}
			}
		}
		templates.sort(Comparator.comparingInt(ZoneTemplate::getMapid));

		ZoneData zoneData = new ZoneData();
		zoneData.zoneList = templates;
		zoneData.saveData();
	}

	public synchronized void createMaterialZoneTemplate(Spatial geometry, int worldId, ZoneName zoneName) {
		if (zoneName == ZoneName.NONE)
			return;

		ZoneHandler handler = collidableHandlers.get(zoneName);
		if (handler == null) {
			if (geometry.getMaterialId() == 11) {
				if (GeoDataConfig.GEO_SHIELDS_ENABLE) {
					handler = new SiegeShield(geometry);
					ShieldService.getInstance().registerShield(worldId, (SiegeShield) handler);
				} else
					return;
			} else {
				MaterialTemplate template = DataManager.MATERIAL_DATA.getTemplate(geometry.getMaterialId());
				if (template == null)
					return;
				handler = new MaterialZoneHandler(geometry, template);
			}
			collidableHandlers.put(zoneName, handler);
		} else {
			log.warn("Duplicate material mesh: " + zoneName.toString());
		}

		List<ZoneInfo> areas = zoneByMapIdMap.get(worldId);
		if (areas == null) {
			areas = new ArrayList<>();
			zoneByMapIdMap.put(worldId, areas);
		}
		ZoneInfo zoneInfo = null;
		for (ZoneInfo area : areas) {
			if (area.getZoneTemplate().getName().equals(zoneName)) {
				zoneInfo = area;
				break;
			}
		}
		if (zoneInfo == null) {
			MaterialZoneTemplate zoneTemplate = new MaterialZoneTemplate(geometry, worldId);
			// maybe add to zone data if needed search ?
			Area zoneInfoArea = null;
			if (zoneTemplate.getSphere() != null) {
				zoneInfoArea = new SphereArea(zoneName, worldId, zoneTemplate.getSphere().getX(), zoneTemplate.getSphere().getY(),
					zoneTemplate.getSphere().getZ(), zoneTemplate.getSphere().getR());
			} else if (zoneTemplate.getCylinder() != null) {
				zoneInfoArea = new CylinderArea(zoneName, worldId, zoneTemplate.getCylinder().getX(), zoneTemplate.getCylinder().getY(),
					zoneTemplate.getCylinder().getR(), zoneTemplate.getCylinder().getBottom(), zoneTemplate.getCylinder().getTop());
			} else if (zoneTemplate.getSemisphere() != null) {
				zoneInfoArea = new SemisphereArea(zoneName, worldId, zoneTemplate.getSemisphere().getX(), zoneTemplate.getSemisphere().getY(),
					zoneTemplate.getSemisphere().getZ(), zoneTemplate.getSemisphere().getR());
			}
			if (zoneInfoArea != null) {
				zoneInfo = new ZoneInfo(zoneInfoArea, zoneTemplate);
				areas.add(zoneInfo);
			}
		}
	}

	public static ZoneService getInstance() {
		return SingletonHolder.instance;
	}

	private static class SingletonHolder {

		protected static final ZoneService instance = new ZoneService();
	}
}
