#pragma once
#include "olcPixelGameEngine.h"
#include "olcUTIL_Geometry2D.h"
#include <strstream>

using namespace olc;

struct XMLTag{
    std::string tag;
    std::map<std::string,std::string> data;
    const std::string FormatTagData(std::map<std::string,std::string>tiles);
    friend std::ostream& operator << (std::ostream& os, XMLTag& rhs);
    std::string str();
    int GetInteger(std::string dataTag);
    float GetFloat(std::string dataTag);
    double GetDouble(std::string dataTag);
    bool GetBool(std::string dataTag);
};

struct MapTag{
    int width=0,height=0;
    vi2d playerSpawnLocation;
    friend std::ostream& operator << (std::ostream& os, MapTag& rhs);
};

struct LayerTag{
    XMLTag tag;
    std::vector<std::vector<int>> tiles;
    std::string str();
};

struct SpawnerTag{
    XMLTag ObjectData;
    std::vector<XMLTag>monsters;
    bool upperLevel=false;
    std::string str();
    friend std::ostream& operator << (std::ostream& os, SpawnerTag& rhs);
};

struct Map{
    MapTag MapData;
    std::vector<XMLTag> TilesetData;
    std::vector<LayerTag> LayerData;
    std::map<int,SpawnerTag> SpawnerData; //Spawn groups have IDs, mobs associate which spawner they are tied to via this ID.
    std::map<std::string,std::vector<geom2d::rect<int>>> ZoneData;
    std::string FormatLayerData(std::ostream& os, std::vector<LayerTag>tiles);
    std::string FormatSpawnerData(std::ostream& os, std::map<int,SpawnerTag>tiles);
    friend std::ostream& operator << (std::ostream& os, Map& rhs);
    friend std::ostream& operator << (std::ostream& os, std::vector<XMLTag>& rhs);
};

class TMXParser{
    public:
    Map GetData();
    private:
    Map parsedMapInfo;
    bool buildingSpawner=false;
    SpawnerTag obj;
    void ParseTag(std::string tag);
    int monsterPropertyTagCount=-1;
    XMLTag monsterTag;
    XMLTag spawnerLinkTag;
    std::vector<XMLTag>accumulatedMonsterTags;
    bool infiniteMap=false;
    public:
    TMXParser(std::string file);
};

typedef std::map<std::string,std::vector<geom2d::rect<int>>> ZoneData;

#ifdef TMX_PARSER_SETUP
#undef TMX_PARSER_SETUP
    const std::string XMLTag::FormatTagData(std::map<std::string,std::string>tiles){
        std::string displayStr="";
        for (std::map<std::string,std::string>::iterator it=data.begin();it!=data.end();it++) {
            displayStr+="  "+it->first+": "+it->second+"\n";
        }
        return displayStr;
    }
    std::ostream& operator << (std::ostream& os, XMLTag& rhs){ 
        os << 
            rhs.tag <<"\n"<< 
            rhs.FormatTagData(rhs.data) <<"\n";
        return os;
    }
    std::ostream& operator << (std::ostream& os, MapTag& rhs){ 
        os << 
            "(width:"<<rhs.width<<", height:"<<rhs.height<<",playerSpawnLocation:"<<rhs.playerSpawnLocation<<")\n";
        return os;
    }
    int XMLTag::GetInteger(std::string dataTag) {
        return std::stoi(data[dataTag]);
    }
    float XMLTag::GetFloat(std::string dataTag) {
        return std::stof(data[dataTag]);
    }
    double XMLTag::GetDouble(std::string dataTag) {
        return std::stod(data[dataTag]);
    }
    bool XMLTag::GetBool(std::string dataTag) {
        if (data[dataTag]=="0") {
            return false;
        } else {
            return true;
        }
    }
    std::string XMLTag::str() {
        return tag+"\n";
    }
    std::string LayerTag::str() {
        std::string displayStr=tag.tag+"\n"+tag.FormatTagData(tag.data);
        displayStr+="  DATA ("+std::to_string(tiles[0].size())+"x"+std::to_string(tiles.size())+"  )\n";
        return displayStr;
    }
    std::string SpawnerTag::str() {
        std::string displayStr=ObjectData.tag+"\n"+ObjectData.FormatTagData(ObjectData.data);
        for(XMLTag&monster:monsters){
            displayStr+="  ("+monster.FormatTagData(monster.data)+"  )\n";
        }
        return displayStr;
    }
    std::ostream& operator << (std::ostream& os, SpawnerTag& rhs) { 
        os << rhs.str()<<"\n";
        return os;
    }
    std::string Map::FormatLayerData(std::ostream& os, std::vector<LayerTag>tiles) {
        std::string displayStr;
        for (int i=0;i<LayerData.size();i++) {
            displayStr+=LayerData[i].str();
        }
        return displayStr;
    }
    std::string Map::FormatSpawnerData(std::ostream& os, std::map<int,SpawnerTag>tiles) {
        std::string displayStr;
        for (auto key:SpawnerData) {
            displayStr+=SpawnerData[key.first].str();
        }
        return displayStr;
    }
    std::ostream& operator <<(std::ostream& os, std::vector<XMLTag>& rhs) { 
        for(XMLTag&tag:rhs){
            os << 
            tag<<"\n";
        }
        return os;
    }
    std::ostream& operator <<(std::ostream& os, Map& rhs) { 
        os << 
            rhs.MapData <<"\n"<< 
            rhs.TilesetData <<"\n"<< 
            rhs.FormatLayerData(os,rhs.LayerData) <<"\n"<<
            rhs.FormatSpawnerData(os,rhs.SpawnerData)<<"\n";
        return os;
    }
    Map TMXParser::GetData() {
        return parsedMapInfo;
    }
    void TMXParser::ParseTag(std::string tag) {
        

        auto ReadNextTag=[&](){
            XMLTag newTag;
            //First character is a '<' so we discard it.
            tag.erase(0,1); tag.erase(tag.length()-1,1); //Erase the first and last characters in the tag. Now parse by spaces.
            std::stringstream s(tag); //Turn it into a string stream to now parse into individual whitespaces.
            std::string data;
            while (s.good()) {
                int quotationMarkCount=0;
                bool pastEquals=false;
                data="";
                bool valid=false;
                while(s.good()){
                    int character=s.get();
                    if(character=='"'){
                        quotationMarkCount++;
                    }
                    if(character==' '&&quotationMarkCount%2==0){
                        valid=true;
                        break;
                    }
                    data+=character;
                    if(pastEquals&&quotationMarkCount%2==0){
                        valid=true;
                        break;
                    }
                    if(character=='='&&quotationMarkCount%2==0){
                        pastEquals=true;
                    }
                }
                if(valid&&data.length()>0){
                    if (newTag.tag.length()==0) { //Tag's empty, so first line is the tag.
                        newTag.tag=data;
                        std::cout<<"Tag: "<<newTag.tag<<"\n";
                    } else {
                        std::string key = data.substr(0,data.find("="));
                        std::string value = data.substr(data.find("=")+1,std::string::npos);

                        //Strip Quotation marks.
                        value = value.substr(1,std::string::npos);
                        value = value.substr(0,value.length()-1);

                        newTag.data[key]=value;
                        std::cout<<"  "<<key<<":"<<newTag.data[key]<<"\n";
                    }
                }
            }
            return newTag;
        };

        XMLTag newTag=ReadNextTag();

        if (newTag.tag=="map") {
            if(stoi(newTag.data["infinite"])==1){
                infiniteMap=true;
                return;
            }
            parsedMapInfo.MapData={stoi(newTag.data["width"]),stoi(newTag.data["height"])};
        } else 
        if (newTag.tag=="tileset") {
            parsedMapInfo.TilesetData.push_back(newTag);
        } else 
        if (newTag.tag=="layer") {
            LayerTag l = {newTag};
            parsedMapInfo.LayerData.push_back(l);
        }else 
        if (newTag.tag=="object"&&newTag.data["type"]=="SpawnGroup") {
            parsedMapInfo.SpawnerData[newTag.GetInteger("id")]={newTag};
        } else 
        if (newTag.tag=="object"&&newTag.data["type"]=="PlayerSpawnLocation") {
            parsedMapInfo.MapData.playerSpawnLocation={newTag.GetInteger("x")-newTag.GetInteger("width")/2,newTag.GetInteger("y")-newTag.GetInteger("height")/2};
        } else 
        if (newTag.tag=="object"&&newTag.data.find("type")!=newTag.data.end()
            &&newTag.data.find("x")!=newTag.data.end()
            &&newTag.data.find("y")!=newTag.data.end()
            &&newTag.data.find("width")!=newTag.data.end()
            &&newTag.data.find("height")!=newTag.data.end()){
            //This is an object with a type that doesn't fit into other categories, we can add it to ZoneData.
            if(parsedMapInfo.ZoneData.find(newTag.data["type"])!=parsedMapInfo.ZoneData.end()){
                std::vector<geom2d::rect<int>>&zones=parsedMapInfo.ZoneData[newTag.data["type"]];
                zones.push_back({{newTag.GetInteger("x"),newTag.GetInteger("y")},{newTag.GetInteger("width"),newTag.GetInteger("height")}});
            } else {
                std::vector<geom2d::rect<int>>zones;
                zones.push_back({{newTag.GetInteger("x"),newTag.GetInteger("y")},{newTag.GetInteger("width"),newTag.GetInteger("height")}});
                parsedMapInfo.ZoneData[newTag.data["type"]]=zones;  
            }
        }else
        if (newTag.tag=="object"&&newTag.data["type"]=="Monster") {
            //XMLTag monsterTag=ReadNextTag();
            //XMLTag spawnerLinkTag=ReadNextTag();
            //newTag.data["value"]=monsterTag.GetInteger("value"); //Value now contains which monster name this spawn represents.
            monsterTag=newTag;
            monsterPropertyTagCount=0;
        } else
        if (newTag.tag=="property"&&monsterPropertyTagCount==0) {
            monsterTag.data["value"]=newTag.data["value"];
            monsterPropertyTagCount++;
        } else 
        if (newTag.tag=="property"&&monsterPropertyTagCount==1) {
            spawnerLinkTag=newTag;
            monsterTag.data["spawnerLink"]=spawnerLinkTag.data["value"];
            accumulatedMonsterTags.push_back(monsterTag);
            monsterPropertyTagCount=-1;
        } else {
            std::cout<<"Unsupported tag format! Ignoring."<<"\n";
        }
        std::cout<<"\n"<<"=============\n";
    }
    TMXParser::TMXParser(std::string file){
        std::ifstream f(file,std::ios::in);

        std::string accumulator="";

        while (f.good()&&!infiniteMap) {
            std::string data;
            f>>data;
            if (data.empty()) continue;

            if (accumulator.length()>0) {
                accumulator+=" "+data;
                //Check if it ends with '>'
                if (data[data.length()-1]=='>') {
                    ParseTag(accumulator);
                    accumulator="";
                }
            } else
                if (data[0]=='<') {
                    //Beginning of XML tag.
                    accumulator=data;
                    if(accumulator.length()>1&&accumulator.at(1)=='/'){
                        accumulator=""; //Restart because this is an end tag.
                    }
                    if(accumulator.length()>1&&accumulator.find('>')!=std::string::npos){
                        accumulator=""; //Restart because this tag has nothing in it!
                    }
                } else {
                    //Start reading in data for this layer.
                    std::vector<int>rowData;
                    while (data.find(",")!=std::string::npos) {
                        std::string datapiece = data.substr(0,data.find(","));
                        data = data.substr(data.find(",")+1,std::string::npos);
                        rowData.push_back(stoi(datapiece));
                    }
                    if (data.length()) {
                        rowData.push_back(stoi(data));
                    }
                    parsedMapInfo.LayerData[parsedMapInfo.LayerData.size()-1].tiles.push_back(rowData);
                }
        }

        if(infiniteMap){
            std::cout<<"Infinite map detected. Parsing stopped early."<<std::endl;
        }

        for(XMLTag&monster:accumulatedMonsterTags){
            parsedMapInfo.SpawnerData[monster.GetInteger("spawnerLink")].monsters.push_back(monster);
        }

        for(auto&spawnerData:parsedMapInfo.SpawnerData){
            SpawnerTag&spawner=spawnerData.second;
            for(auto&zoneData:parsedMapInfo.ZoneData){
                if(zoneData.first=="UpperZone"){
                    std::vector<geom2d::rect<int>>&zones=zoneData.second;
                    for(geom2d::rect<int>&zone:zones){
                        if(geom2d::overlaps(zone,geom2d::rect<int>{{spawner.ObjectData.GetInteger("x"),spawner.ObjectData.GetInteger("y")},{spawner.ObjectData.GetInteger("width"),spawner.ObjectData.GetInteger("height")}})){
                            spawner.upperLevel=true;
                            goto continueSpawnerLoop;
                        }
                    }
                }
            }
            continueSpawnerLoop:
            continue;
        }
        std::cout<<"Parsed Map Data:\n"<<parsedMapInfo<<"\n";
    }
#endif