package crazypants.enderzoo.config;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.io.IOUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import crazypants.enderzoo.Log;
import crazypants.enderzoo.spawn.IBiomeFilter;
import crazypants.enderzoo.spawn.impl.BiomeDescriptor;
import crazypants.enderzoo.spawn.impl.BiomeFilterAll;
import crazypants.enderzoo.spawn.impl.BiomeFilterAny;
import crazypants.enderzoo.spawn.impl.DimensionFilter;
import crazypants.enderzoo.spawn.impl.SpawnEntry;
import net.minecraft.entity.EnumCreatureType;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.MathHelper;
import net.minecraftforge.common.BiomeDictionary;
import net.minecraftforge.common.BiomeDictionary.Type;

public class SpawnConfigParser extends DefaultHandler {

  public static List<SpawnEntry> parseSpawnConfig(String text) throws Exception {
    StringReader sr = new StringReader(text);
    InputSource is = new InputSource(sr);
    try {
      return parse(is);
    } finally {
      IOUtils.closeQuietly(sr);
    }
  }

  public static List<SpawnEntry> parseSpawnConfig(File file) throws Exception {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
    InputSource is = new InputSource(bis);
    try {
      return parse(is);
    } finally {
      IOUtils.closeQuietly(bis);
    }
  }

  public static List<SpawnEntry> parse(InputSource is) throws Exception {

    SpawnConfigParser parser = new SpawnConfigParser();

    SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    SAXParser saxParser = spf.newSAXParser();
    XMLReader xmlReader = saxParser.getXMLReader();
    xmlReader.setContentHandler(parser);
    xmlReader.parse(is);

    return parser.getResult();
  }

  //----------------------- Parser -----------------------------------------------------

  public static final String ELEMENT_ROOT = "SpawnConfig";
  public static final String ELEMENT_ENTRY = "entry";
  public static final String ELEMENT_FILTER = "biomeFilter";
  public static final String ELEMENT_BIOME = "biome";

  public static final String ELEMENT_DIM_EXCLUDE = "dimensionExclude";

  public static final String ATT_ID = "id";
  public static final String ATT_ID_START = "idStart";
  public static final String ATT_ID_END = "idEnd";
  public static final String ATT_MOB_NAME = "mobName";
  public static final String ATT_CREATURE_TYPE = "creatureType";
  public static final String ATT_RATE = "rate";
  public static final String ATT_MIN_GRP = "minGroupSize";
  public static final String ATT_MAX_GRP = "maxGroupSize";
  public static final String ATT_REMOVE = "remove";

  public static final String ATT_NAME = "name";
  public static final String ATT_TYPE = "type";
  public static final String ATT_EXCLUDE = "exclude";

  private static final String FILTER_TYPE_ANY = "any";
  private static final String FILTER_TYPE_ALL = "all";

  public static final String BASE_LAND_TYPES = "BASE_LAND_TYPES";

  private static final BiomeDictionary.Type[] BASE_LAND_TYPES_ARR = new BiomeDictionary.Type[] { BiomeDictionary.Type.MESA, BiomeDictionary.Type.FOREST,
      BiomeDictionary.Type.PLAINS, BiomeDictionary.Type.MOUNTAIN, BiomeDictionary.Type.HILLS, BiomeDictionary.Type.SWAMP, BiomeDictionary.Type.SANDY,
      BiomeDictionary.Type.SNOWY, BiomeDictionary.Type.WASTELAND, BiomeDictionary.Type.BEACH, };

  private final List<SpawnEntry> result = new ArrayList<SpawnEntry>();

  private SpawnEntry currentEntry;
  private IBiomeFilter currentFilter;
  private boolean invalidEntryElement = false;
  private boolean foundRoot = false;
  private boolean documentedClosed = false;
  private boolean printedDocumentClosedWarn = false;

  public List<SpawnEntry> getResult() {
    return result;
  }

  @Override
  public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
    if (documentedClosed) {
      if (!printedDocumentClosedWarn) {
        Log.warn("Elements found after closing " + ELEMENT_ROOT + " they will be ignroed.");
        printedDocumentClosedWarn = true;
      }
      return;
    }
    if (ELEMENT_ROOT.equals(localName)) {
      if (foundRoot) {
        Log.warn("Mulitple " + ELEMENT_ROOT + " elements found.");
      }
      foundRoot = true;
    } else if (ELEMENT_ENTRY.equals(localName)) {
      if (!foundRoot) {
        Log.warn("Element " + ELEMENT_ENTRY + " found before " + ELEMENT_ROOT);
      }
      if (currentEntry != null) {
        Log.warn("New " + ELEMENT_ENTRY + " found before previous element closed. Discarding " + currentEntry);
      }
      parseEntry(attributes);

    } else if (ELEMENT_FILTER.equals(localName)) {
      if (!foundRoot) {
        Log.warn("Element " + ELEMENT_FILTER + " found before " + ELEMENT_ROOT);
      }
      if (currentEntry == null) {
        if (!invalidEntryElement) {
          Log.warn(ELEMENT_FILTER + " found outside an " + ELEMENT_ENTRY + " element. It will be ignored.");
        }
        return;
      }
      parseFilter(attributes);

    } else if (ELEMENT_BIOME.equals(localName)) {
      if (!foundRoot) {
        Log.warn("Element " + ELEMENT_BIOME + " found before " + ELEMENT_ROOT);
      }
      if ((currentEntry == null || currentFilter == null) && !invalidEntryElement) {
        Log.warn(ELEMENT_BIOME + " found outside an " + ELEMENT_ENTRY + " and/or " + ELEMENT_FILTER + " element. It will be ignored");
      }
      if (!invalidEntryElement && currentFilter != null) {
        parseBiomeType(attributes);
      }
    } else if (ELEMENT_DIM_EXCLUDE.equals(localName)) {
      if (!foundRoot) {
        Log.warn("Element " + ELEMENT_DIM_EXCLUDE + " found before " + ELEMENT_ROOT);
      }
      if (currentEntry == null && !invalidEntryElement) {
        Log.warn(ELEMENT_DIM_EXCLUDE + " found outside an " + ELEMENT_ENTRY + " and/or " + ELEMENT_FILTER + " element. It will be ignored");
      } else if (currentEntry != null) {
        parseDimExclude(attributes);
      }
    }
  }

  private void parseDimExclude(Attributes attributes) {
    String name = getStringValue(ATT_NAME, attributes, null);
    if (name != null) {
      currentEntry.addDimensionFilter(new DimensionFilter(name));
      return;
    }
    int id = getIntValue(ATT_ID, attributes, Integer.MAX_VALUE);
    if (id != Integer.MAX_VALUE) {
      currentEntry.addDimensionFilter(new DimensionFilter(id));
      return;
    }
    currentEntry.addDimensionFilter(new DimensionFilter(getIntValue(ATT_ID_START, attributes, Integer.MAX_VALUE), getIntValue(ATT_ID_END, attributes,
        Integer.MAX_VALUE)));

  }

  @Override
  public void endElement(String uri, String localName, String qName) throws SAXException {
    if (ELEMENT_ENTRY.equals(localName) && currentEntry != null) {
      if (currentEntry != null) {
        result.add(currentEntry);
      }
      currentEntry = null;
    } else if (ELEMENT_FILTER.equals(localName)) {
      if (currentFilter != null && currentEntry != null) {
        currentEntry.addBiomeFilter(currentFilter);
      }
      currentFilter = null;
    } else if (ELEMENT_ROOT.equals(localName)) {
      documentedClosed = true;
    }
  }

  private void parseEntry(Attributes attributes) {
    invalidEntryElement = false;
    String id = getStringValue(ATT_ID, attributes, null);
    if (id == null) {
      Log.error(ELEMENT_ENTRY + " specified without an " + ATT_ID + " atribute");
      invalidEntryElement = true;
      return;
    }
    String mobName = getStringValue(ATT_MOB_NAME, attributes, null);
    if (isEmptyString(mobName)) {
      Log.error(ELEMENT_ENTRY + " specified without an " + ATT_MOB_NAME + " atribute");
      invalidEntryElement = true;
      return;
    }
    mobName = mobName.trim();

    int rate = getIntValue(ATT_RATE, attributes, -1);
    if (rate <= 0) {
      Log.error(ELEMENT_ENTRY + " specified without a valid " + ATT_RATE + " atribute");
      invalidEntryElement = true;
      return;
    }
    rate = MathHelper.clamp(rate, 1, 100);

    currentEntry = new SpawnEntry(id, mobName, rate);
    String creatureType = getStringValue(ATT_CREATURE_TYPE, attributes, null);
    if (creatureType != null) {
      try {
        currentEntry.setCreatureType(EnumCreatureType.valueOf(creatureType.trim()));
      } catch (Exception e) {
        Log.warn("Invalid value specified for " + ATT_CREATURE_TYPE + " in entry " + id + " using default value " + currentEntry.getCreatureType() + " error: "
            + e);
      }
    }

    int minGrp = getIntValue(ATT_MIN_GRP, attributes, -1);
    if (minGrp != -1) {
      if (minGrp < 0) {
        Log.warn("Value less than 0 found for " + ATT_MIN_GRP + " in entry " + id + " using default value " + currentEntry.getMinGroupSize());
      } else {
        currentEntry.setMinGroupSize(minGrp);
      }
    }

    int maxGrp = getIntValue(ATT_MAX_GRP, attributes, -1);
    if (maxGrp != -1) {
      if (maxGrp < currentEntry.getMinGroupSize()) {
        Log.warn("Value for " + ATT_MAX_GRP + " in entry " + id + " less than " + ATT_MIN_GRP + " using default " + currentEntry.getMaxGroupSize());
      } else {
        currentEntry.setMaxGroupSize(maxGrp);
      }
    }

    currentEntry.setIsRemove(getBooleanValue(ATT_REMOVE, attributes, currentEntry.isRemove()));

  }

  private void parseFilter(Attributes attributes) {
    String typeStr = getStringValue(ATT_TYPE, attributes, null);
    if (isEmptyString(typeStr)) {
      Log.warn("Attribue " + ATT_TYPE + " not specified for element " + ELEMENT_FILTER + " defaulting to '" + FILTER_TYPE_ANY + "' filter");
      typeStr = FILTER_TYPE_ANY;
    }

    if (FILTER_TYPE_ANY.equals(typeStr)) {
      currentFilter = new BiomeFilterAny();
    } else if (FILTER_TYPE_ALL.equals(typeStr)) {
      currentFilter = new BiomeFilterAll();
    }

    if (currentFilter == null) {
      Log.warn("Unknown " + ATT_TYPE + " '" + typeStr + "' specified for filter. Filter will be ignored.");
    }

  }

  private void parseBiomeType(Attributes attributes) {
    String biomeName = getStringValue(ATT_NAME, attributes, null);
    boolean nameEmpty = isEmptyString(biomeName);
    String biomeType = getStringValue(ATT_TYPE, attributes, null);
    boolean typeEmpty = isEmptyString(biomeType);
    if (nameEmpty && typeEmpty) {
      Log.warn("Attribute " + ATT_NAME + " or " + ATT_TYPE + " not specified in element " + ELEMENT_BIOME + " in entry " + currentEntry.getId());
      return;
    }
    if (!nameEmpty && !typeEmpty) {
      Log.warn("Attribute " + ATT_NAME + " and " + ATT_TYPE + " both specified in element " + ELEMENT_BIOME + " in entry " + currentEntry.getId()
          + ". It will be ignored");
      return;
    }

    boolean isExclude = getBooleanValue(ATT_EXCLUDE, attributes, false);
    if (!typeEmpty) {
      biomeType = biomeType.trim();
      if (BASE_LAND_TYPES.equals(biomeType)) {
        for (BiomeDictionary.Type type : BASE_LAND_TYPES_ARR) {
          currentFilter.addBiomeDescriptor(new BiomeDescriptor(type, isExclude));
        }
      } else {
        try {
          Type type = BiomeDictionary.Type.getType(biomeType);
          currentFilter.addBiomeDescriptor(new BiomeDescriptor(type, isExclude));
        } catch (Exception e) {
          Log.warn("Attribute " + ATT_TYPE + " in element " + ELEMENT_BIOME + " with value " + biomeType + " is invalid and has been ignored.");
        }
      }
      return;
    }
    currentFilter.addBiomeDescriptor(new BiomeDescriptor(new ResourceLocation(biomeName.trim()), isExclude));
  }

  protected boolean isEmptyString(String str) {
    return str == null || str.trim().length() == 0;
  }

  @Override
  public void warning(SAXParseException e) throws SAXException {
    Log.warn("Warning parsing Spawn config file: " + e.getMessage());
  }

  @Override
  public void error(SAXParseException e) throws SAXException {
    Log.error("Error parsing Spawn config file: " + e.getMessage());
    e.printStackTrace();
  }

  @Override
  public void fatalError(SAXParseException e) throws SAXException {
    Log.error("Error parsing Spawn config file: " + e.getMessage());
    e.printStackTrace();
  }

  public static boolean getBooleanValue(String qName, Attributes attributes, boolean def) {
    String val = attributes.getValue(qName);
    if (val == null) {
      return def;
    }
    val = val.toLowerCase().trim();
    return val.equals("false") ? false : val.equals("true") ? true : def;
  }

  public static int getIntValue(String qName, Attributes attributes, int def) {
    try {
      return Integer.parseInt(getStringValue(qName, attributes, def + ""));
    } catch (Exception e) {
      Log.warn("Could not parse a valid int for attribute " + qName + " with value " + getStringValue(qName, attributes, null));
      return def;
    }
  }

  public static float getFloatValue(String qName, Attributes attributes, float def) {
    try {
      return Float.parseFloat(getStringValue(qName, attributes, def + ""));
    } catch (Exception e) {
      Log.warn("Could not parse a valid float for attribute " + qName + " with value " + getStringValue(qName, attributes, null));
      return def;
    }
  }

  public static String getStringValue(String qName, Attributes attributes, String def) {
    String val = attributes.getValue(qName);
    if (val == null) {
      return def;
    }
    val = val.trim();
    if (val.length() <= 0) {
      return null;
    }
    return val;
  }

}