#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~

Copyright 2018 - 2023 OneLoneCoder.com

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following	disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS	"AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT	HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL,	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
*/
#pragma endregion
#pragma once
#include "olcPixelGameEngine.h"
#include "olcUTIL_Geometry2D.h"
#include <sstream>
#include <set>

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;
    int tilewidth=0,tileheight=0;
    bool optimized=false; //An optimized map will require us to flatten it out and use it as a single tile.
    vi2d playerSpawnLocation;
    vi2d MapSize; //The number of tiles in width and height of this map.
    vi2d TileSize; //How large in pixels the map's tiles are.
    MapTag();
    MapTag(int width,int height,int tilewidth,int tileheight);
    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 bossNameDisplay;
    std::string str();
    friend std::ostream& operator << (std::ostream& os, SpawnerTag& rhs);
};

struct ZoneData{
    geom2d::rect<int>zone;
    bool isUpper=false;
};

struct Map{
    MapTag MapData;
    std::string name;
    Renderable*optimizedTile=nullptr;
    std::vector<XMLTag> TilesetData;
    std::vector<LayerTag> LayerData;
    std::string mapType="";
    std::set<int>spawns;
    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<ZoneData>> 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);
};

struct Property{
    std::string name;
    std::string value;
    int GetInteger();
    float GetFloat();
    double GetDouble();
    bool GetBool();
};

struct StagePlate{
    XMLTag tag;
    std::map<std::string,Property>properties;
};

class TMXParser{
    public:
    Map GetData();
    private:
    Map parsedMapInfo;
    bool buildingSpawner=false;
    SpawnerTag obj;
    int prevSpawner;
    ZoneData*prevZoneData=nullptr;
    void ParseTag(std::string tag);
    int monsterPropertyTagCount=-1;
    XMLTag monsterTag;
    XMLTag spawnerLinkTag;
    StagePlate*currentStagePlate;
    std::vector<XMLTag>accumulatedMonsterTags;
    std::map<int,StagePlate>stagePlates;
    bool infiniteMap=false;
    public:
    TMXParser(std::string file);
};

//#define TMX_PARSER_SETUP //Toggle for code-writing.

#ifdef TMX_PARSER_SETUP
#undef TMX_PARSER_SETUP
    #include "State_OverworldMap.h"
    extern bool _DEBUG_MAP_LOAD_INFO;
    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<<", Tile width:"<<rhs.tilewidth<<", Tile height:"<<rhs.tileheight<<",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"||data[dataTag]=="false") {
            return false;
        } else {
            return true;
        }
    }
    int Property::GetInteger() {
        return std::stoi(value);
    }
    float Property::GetFloat() {
        return std::stof(value);
    }
    double Property::GetDouble() {
        return std::stod(value);
    }
    bool Property::GetBool() {
        if (value=="0") {
            return false;
        } else {
            return true;
        }
    }
    MapTag::MapTag(){}
    MapTag::MapTag(int width,int height,int tilewidth,int tileheight)
        :width(width),height(height),tilewidth(tilewidth),tileheight(tileheight),MapSize({width,height}),TileSize({tilewidth,tileheight}){}
    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;
                        if(_DEBUG_MAP_LOAD_INFO)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;
                        if(_DEBUG_MAP_LOAD_INFO)std::cout<<"  "<<key<<":"<<newTag.data[key]<<"\n";
                    }
                }
            }
            return newTag;
        };

        XMLTag newTag=ReadNextTag();
        
        if (newTag.tag=="object"&&newTag.data["type"]!="StagePlate"){
            currentStagePlate=nullptr;
        }

        if (newTag.tag=="map"){
            if(stoi(newTag.data["infinite"])==1){
                infiniteMap=true;
                return;
            }
            parsedMapInfo.MapData={stoi(newTag.data["width"]),stoi(newTag.data["height"]),stoi(newTag.data["tilewidth"]),stoi(newTag.data["tileheight"])};
        } 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"){
            if(newTag.GetInteger("id")!=0){
                parsedMapInfo.SpawnerData[newTag.GetInteger("id")]={newTag};
                prevSpawner=newTag.GetInteger("id");
            }
        } else 
        if(newTag.tag=="property"&&newTag.data["name"]=="Upper?"&&prevZoneData!=nullptr){
            prevZoneData->isUpper=newTag.GetBool("value");
        }else
        if (newTag.tag=="property"&&newTag.data["name"]=="Optimize"&&newTag.data["value"]=="true") {
            parsedMapInfo.MapData.optimized=true;
        } else 
        if (newTag.tag=="property"&&newTag.data["name"]=="Boss Title Display") {
            parsedMapInfo.SpawnerData[prevSpawner].bossNameDisplay=newTag.data["value"];
        } else 
        if (newTag.tag=="property"&&newTag.data["name"]=="Level Type") {
            parsedMapInfo.mapType=newTag.data["value"];
        } 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["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"];
            parsedMapInfo.spawns.insert(newTag.GetInteger("value"));
            monsterPropertyTagCount++;
        } else 
        if (newTag.tag=="property"&&monsterPropertyTagCount==1) {
            spawnerLinkTag=newTag;
            monsterTag.data["spawnerLink"]=spawnerLinkTag.data["value"];
            accumulatedMonsterTags.push_back(monsterTag);
            monsterPropertyTagCount=-1;
        } else 
        if (newTag.tag=="object"&&newTag.data["type"]=="StagePlate") {
            if(newTag.GetInteger("id")!=0){
                stagePlates[newTag.GetInteger("id")]={newTag};
                currentStagePlate=&stagePlates.at(newTag.GetInteger("id"));
            }
        } else 
        if(newTag.tag=="property"&&currentStagePlate!=nullptr){
            currentStagePlate->properties[newTag.data["name"]]={newTag.data["name"],newTag.data["value"]};
        }else
        if (newTag.tag=="object"&&newTag.data.find("type")!=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<ZoneData>&zones=parsedMapInfo.ZoneData.at(newTag.data["type"]);
                zones.emplace_back(geom2d::rect<int>{{newTag.GetInteger("x"),newTag.GetInteger("y")},{newTag.GetInteger("width"),newTag.GetInteger("height")}});
                prevZoneData=&zones.back();
            } else {
                std::vector<ZoneData>&zones=parsedMapInfo.ZoneData[newTag.data["type"]];
                zones.emplace_back(geom2d::rect<int>{{newTag.GetInteger("x"),newTag.GetInteger("y")},{newTag.GetInteger("width"),newTag.GetInteger("height")}});
                prevZoneData=&zones.back();
            }
        }else{
            if(_DEBUG_MAP_LOAD_INFO)std::cout<<"Unsupported tag format! Ignoring."<<"\n";
        }
        if(_DEBUG_MAP_LOAD_INFO)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){
            if(_DEBUG_MAP_LOAD_INFO)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<ZoneData>&zones=zoneData.second;
                    for(ZoneData&zone:zones){
                        if(geom2d::overlaps(zone.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::sort(parsedMapInfo.TilesetData.begin(),parsedMapInfo.TilesetData.end(),[](XMLTag&t1,XMLTag&t2){return t1.GetInteger("firstgid")<t2.GetInteger("firstgid");});

        std::map<int,int>idToIndexMap;  //Since the original map data relies on IDs decided by Tiled and we are condensing all this data into a vector of connection points, each connection point is going to be in a different ID.
                                        //therefore, we need to convert the Tiled IDs into whatever vector index we insert each connection into for State_OverworldMap::connections.
        for(auto key:stagePlates){
            StagePlate&plate=key.second;
            idToIndexMap[plate.tag.GetInteger("id")]=int(State_OverworldMap::connections.size());
            ConnectionPoint newConnection={{{plate.tag.GetFloat("x"),plate.tag.GetFloat("y")},{plate.tag.GetFloat("width"),plate.tag.GetFloat("height")}},plate.tag.data["name"],plate.properties["Map"].value,plate.properties["Unlock Condition"].value,{}};
            int iterationCount=0;
            for(auto key2:plate.properties){
                if(key2.first.starts_with("Connection ")){
                    newConnection.neighbors[iterationCount]=key2.second.GetInteger();
                    iterationCount++;
                }
            }
            State_OverworldMap::connections.push_back(newConnection);
        }
        for(ConnectionPoint&connection:State_OverworldMap::connections){
            for(int&val:connection.neighbors){
                if(idToIndexMap.count(val)){
                    val=idToIndexMap.at(val); //Convert from given Tiled ID to indexed ID in State_OverworldMap::connections
                }
            }
        }
        int counter=0;
        for(ConnectionPoint&connection:State_OverworldMap::connections){
            for(int val:connection.neighbors){
                if(val!=-1){
                    ConnectionPoint&neighbor=State_OverworldMap::connections.at(val);
                    //Find a blank slot that is available.
                    for(int i=0;i<neighbor.neighbors.size();i++){
                        if(neighbor.neighbors[i]==-1){ //We insert our neighbor pairing here.
                            neighbor.neighbors[i]=counter;
                            break;
                        }
                    }
                }
            }
            counter++;
        }

        if(_DEBUG_MAP_LOAD_INFO)std::cout<<"Parsed Map Data:\n"<<parsedMapInfo<<"\n";
    }
#endif