package space.animefu.starfallhaven;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.CRC32;
import static space.animefu.starfallhaven.ColonySystems.*;

/** Versioned, checksummed save codec. Version 4 reads and deterministically migrates v1/v2/v3 colonies. */
public final class GameWorldSave {
    private static final int MAGIC=0x53464856; // SFHV
    private static final int MAX_PAYLOAD=4_000_000;
    public static final int VERSION=4;
    private GameWorldSave(){}

    public static final class SaveException extends Exception {
        SaveException(String message){super(message);} SaveException(String message,Throwable cause){super(message,cause);}
    }

    public static byte[] encode(GameWorld w)throws SaveException{
        try{
            ByteArrayOutputStream payloadBytes=new ByteArrayOutputStream();DataOutputStream out=new DataOutputStream(payloadBytes);
            synchronized(w){
                writeCommon(out,w);out.writeInt(w.components);out.writeLong(w.nextEntityId);
                out.writeInt(w.colonists.size());for(GameWorld.Colonist c:w.colonists)writeColonistV2(out,c);
                out.writeInt(w.enemies.size());for(GameWorld.Enemy e:w.enemies){out.writeLong(e.id);out.writeFloat(e.x);out.writeFloat(e.y);out.writeFloat(e.health);}
                out.writeInt(w.blueprints.size());for(Blueprint b:w.blueprints){out.writeLong(b.id);out.writeByte(b.type.ordinal());out.writeInt(b.x);out.writeInt(b.y);out.writeFloat(b.workDone);out.writeFloat(b.workRequired);out.writeBoolean(b.complete);}
                out.writeInt(w.stockpiles.size());for(StockpileZone z:w.stockpiles){out.writeLong(z.id);out.writeInt(z.x);out.writeInt(z.y);out.writeInt(z.width);out.writeInt(z.height);out.writeLong(enumMask(z.allowed));}
                out.writeInt(w.resourcePiles.size());for(ResourcePile p:w.resourcePiles){out.writeLong(p.id);out.writeByte(p.type.ordinal());out.writeInt(p.amount);out.writeFloat(p.x);out.writeFloat(p.y);out.writeBoolean(p.stored);writeNullable(out,p.reservedBy);}
                out.writeInt(w.productionJobs.size());for(ProductionJob j:w.productionJobs){out.writeLong(j.id);out.writeLong(j.benchId);out.writeByte(j.recipe.ordinal());out.writeInt(j.remaining);out.writeFloat(j.workDone);}
                writeRegionalV3(out,w);writeChronicleV4(out,w);
            }
            out.flush();return frame(payloadBytes.toByteArray(),VERSION);
        }catch(IOException|RuntimeException e){throw new SaveException("Could not encode save",e);}
    }

    public static GameWorld decode(byte[] bytes)throws SaveException{
        if(bytes==null)throw new SaveException("Save is missing");
        try{
            DataInputStream frame=new DataInputStream(new ByteArrayInputStream(bytes));if(frame.readInt()!=MAGIC)throw new SaveException("Not a Starfall Haven save");
            int version=frame.readInt();if(version<1||version>VERSION)throw new SaveException("Unsupported save version "+version);
            int length=frame.readInt();if(length<0||length>MAX_PAYLOAD||length!=bytes.length-16)throw new SaveException("Invalid save length");
            byte[] payload=new byte[length];frame.readFully(payload);long expected=Integer.toUnsignedLong(frame.readInt());CRC32 crc=new CRC32();crc.update(payload);if(crc.getValue()!=expected)throw new SaveException("Save checksum failed");
            DataInputStream in=new DataInputStream(new ByteArrayInputStream(payload));GameWorld w=version==1?readV1(in):version==2?readV2(in):version==3?readV3(in):readV4(in);if(version<3)migrateRegional(w);if(version<4)migrateChronicle(w);if(in.available()!=0)throw new SaveException("Save contains trailing data");return w;
        }catch(SaveException e){throw e;}catch(IOException|RuntimeException e){throw new SaveException("Save is corrupt",e);}
    }

    private static void writeCommon(DataOutputStream out,GameWorld w)throws IOException{
        out.writeLong(w.random.state);out.writeInt(w.wood);out.writeInt(w.stone);out.writeInt(w.food);out.writeInt(w.crystal);out.writeInt(w.day);
        out.writeFloat(w.timeOfDay);out.writeFloat(w.beaconCharge);out.writeFloat(w.messageTime);out.writeFloat(w.bondCharge);out.writeFloat(w.bondActiveSeconds);
        out.writeBoolean(w.paused);out.writeFloat(w.speed);out.writeByte(w.tool.ordinal());out.writeByte(w.result.ordinal());out.writeBoolean(w.raidSpawned);writeString(out,w.message);
        for(int y=0;y<GameWorld.HEIGHT;y++)for(int x=0;x<GameWorld.WIDTH;x++){out.writeByte(w.tiles[x][y].ordinal());out.writeBoolean(w.walls[x][y]);}
    }
    private static GameWorld readCommon(DataInputStream in)throws IOException,SaveException{
        long rng=in.readLong();GameWorld w=new GameWorld(rng,false);w.random.state=rng;
        w.wood=in.readInt();w.stone=in.readInt();w.food=in.readInt();w.crystal=in.readInt();w.day=in.readInt();w.timeOfDay=in.readFloat();w.beaconCharge=in.readFloat();w.messageTime=in.readFloat();w.bondCharge=in.readFloat();w.bondActiveSeconds=in.readFloat();
        w.paused=in.readBoolean();w.speed=in.readFloat();w.tool=enumAt(GameWorld.Tool.values(),in.readUnsignedByte(),"tool");w.result=enumAt(GameWorld.Result.values(),in.readUnsignedByte(),"result");w.raidSpawned=in.readBoolean();w.message=readString(in);
        for(int y=0;y<GameWorld.HEIGHT;y++)for(int x=0;x<GameWorld.WIDTH;x++){w.tiles[x][y]=enumAt(GameWorld.Tile.values(),in.readUnsignedByte(),"tile");w.walls[x][y]=in.readBoolean();}return w;
    }
    private static GameWorld readV1(DataInputStream in)throws IOException,SaveException{
        GameWorld w=readCommon(in);int n=count(in,"colonists",32);
        for(int i=0;i<n;i++){String name=readString(in);GameWorld.Colonist c=new GameWorld.Colonist("haven-"+(i+1),name,in.readFloat(),in.readFloat());c.health=in.readFloat();c.hunger=in.readFloat();c.energy=in.readFloat();c.tx=in.readInt();c.ty=in.readInt();c.work=in.readFloat();applyMigrationIdentity(c,i);w.colonists.add(c);}w.nextEntityId=Math.max(w.nextEntityId,n+1L);
        int enemies=count(in,"enemies",10_000);for(int i=0;i<enemies;i++){GameWorld.Enemy e=new GameWorld.Enemy(w.newId(),in.readFloat(),in.readFloat());e.health=in.readFloat();w.enemies.add(e);}w.components=0;return w;
    }
    private static GameWorld readV2(DataInputStream in)throws IOException,SaveException{
        GameWorld w=readCommon(in);w.components=in.readInt();w.nextEntityId=in.readLong();
        int n=count(in,"colonists",32);for(int i=0;i<n;i++)w.colonists.add(readColonistV2(in));
        int enemies=count(in,"enemies",10_000);for(int i=0;i<enemies;i++){GameWorld.Enemy e=new GameWorld.Enemy(in.readLong(),in.readFloat(),in.readFloat());e.health=in.readFloat();w.enemies.add(e);}
        int blueprints=count(in,"blueprints",10_000);for(int i=0;i<blueprints;i++){long id=in.readLong();StructureType type=enumAt(StructureType.values(),in.readUnsignedByte(),"structure");int x=in.readInt(),y=in.readInt();float done=in.readFloat(),required=in.readFloat();Blueprint b=new Blueprint(id,type,x,y,required);b.workDone=done;b.complete=in.readBoolean();w.blueprints.add(b);}
        int zones=count(in,"stockpiles",1_000);for(int i=0;i<zones;i++){long id=in.readLong();int x=in.readInt(),y=in.readInt(),width=in.readInt(),height=in.readInt();w.stockpiles.add(new StockpileZone(id,x,y,width,height,mask(in.readLong(),ResourceType.class)));}
        int piles=count(in,"resource piles",10_000);for(int i=0;i<piles;i++){long id=in.readLong();ResourceType type=enumAt(ResourceType.values(),in.readUnsignedByte(),"resource");ResourcePile p=new ResourcePile(id,type,in.readInt(),in.readFloat(),in.readFloat());p.stored=in.readBoolean();p.reservedBy=readNullable(in);w.resourcePiles.add(p);}
        int jobs=count(in,"production jobs",10_000);for(int i=0;i<jobs;i++){ProductionJob j=new ProductionJob(in.readLong(),in.readLong(),enumAt(Recipe.values(),in.readUnsignedByte(),"recipe"),in.readInt());j.workDone=in.readFloat();w.productionJobs.add(j);}return w;
    }

    private static void migrateRegional(GameWorld w){w.initializeRegion(w.worldSeed);w.factionSimulation.advanceToDay(w.day);w.landingSiteConfirmed=true;w.initializeSociety(w.worldSeed);w.society.families.currentDay=w.day;}
    private static void migrateChronicle(GameWorld w){w.chronicleHistory=new ChronicleHistory();w.chronicleThreat=0;w.chronicleTension=20;w.chronicleNextDay=Math.max(w.day+1,2);}
    private static void writeRegionalV3(DataOutputStream out,GameWorld w)throws IOException{
        out.writeLong(w.worldSeed);out.writeInt(w.selectedLandingX);out.writeInt(w.selectedLandingY);out.writeBoolean(w.landingSiteConfirmed);
        FactionSimulation simulation=w.factionSimulation;out.writeInt(simulation==null?0:simulation.strategicTurn);out.writeInt(simulation==null?0:simulation.factions.size());
        if(simulation!=null)for(FactionSimulation.Faction faction:simulation.factions){writeString(out,faction.id);writeString(out,faction.name);out.writeInt(faction.standing);out.writeInt(faction.settlements.size());for(FactionSimulation.Settlement settlement:faction.settlements){writeString(out,settlement.id);writeString(out,settlement.name);out.writeInt(settlement.x);out.writeInt(settlement.y);out.writeInt(settlement.population);out.writeInt(settlement.prosperity);out.writeInt(settlement.security);out.writeByte(settlement.lastStrategy.ordinal());}}
        out.writeInt(w.colonists.size());for(GameWorld.Colonist c:w.colonists){writeString(out,c.id);out.writeByte(c.ageStage.ordinal());out.writeInt(c.ageDays);}
        ByteArrayOutputStream societyBytes=new ByteArrayOutputStream();try(ObjectOutputStream objects=new ObjectOutputStream(societyBytes)){objects.writeObject(w.society);}byte[] society=societyBytes.toByteArray();if(society.length>1_000_000)throw new IOException("Society state is too large");out.writeInt(society.length);out.write(society);
    }
    private static GameWorld readV3(DataInputStream in)throws IOException,SaveException{
        GameWorld old=readV2(in);long seed=in.readLong();
        // The v2 common state stores only PRNG state, so reconstruct with the explicit v3 world seed.
        GameWorld w=copyWithSeed(old,seed);w.regionalMap=new RegionalMap(seed);w.selectedLandingX=in.readInt();w.selectedLandingY=in.readInt();if(!w.regionalMap.inside(w.selectedLandingX,w.selectedLandingY)||!w.regionalMap.cell(w.selectedLandingX,w.selectedLandingY).canLand())throw new SaveException("Invalid landing site");w.landingSiteConfirmed=in.readBoolean();
        int turn=in.readInt();if(turn<0||turn>1_000_000)throw new SaveException("Invalid strategic turn");FactionSimulation simulation=FactionSimulation.empty(seed,turn);int factions=count(in,"factions",64);
        for(int i=0;i<factions;i++){FactionSimulation.Faction faction=FactionSimulation.addFaction(simulation,readString(in),readString(in),in.readInt());int settlements=count(in,"settlements",1_000);for(int j=0;j<settlements;j++){String id=readString(in),name=readString(in);int x=in.readInt(),y=in.readInt(),population=in.readInt(),prosperity=in.readInt(),security=in.readInt();FactionSimulation.Strategy strategy=enumAt(FactionSimulation.Strategy.values(),in.readUnsignedByte(),"strategy");FactionSimulation.addSettlement(faction,id,name,x,y,population,prosperity,security,strategy);}}
        w.factionSimulation=simulation;int ages=count(in,"colonist ages",32);for(int i=0;i<ages;i++){String id=readString(in);GameWorld.Colonist c=w.colonist(id);SocietySystems.AgeStage stage=enumAt(SocietySystems.AgeStage.values(),in.readUnsignedByte(),"age stage");int age=in.readInt();if(age<0||age>1000*SocietySystems.YEAR_DAYS)throw new SaveException("Invalid colonist age");if(c!=null){c.ageStage=stage;c.ageDays=age;}}
        int societyLength=in.readInt();if(societyLength<0||societyLength>1_000_000)throw new SaveException("Invalid society state length");byte[] societyBytes=new byte[societyLength];in.readFully(societyBytes);try(ObjectInputStream objects=new ObjectInputStream(new ByteArrayInputStream(societyBytes))){Object value=objects.readObject();if(!(value instanceof SocietySystems.SocietyState))throw new SaveException("Invalid society state");w.society=(SocietySystems.SocietyState)value;}catch(ClassNotFoundException e){throw new SaveException("Society state class is unavailable",e);}return w;
    }
    private static void writeChronicleV4(DataOutputStream out,GameWorld w)throws IOException{
        out.writeInt(w.chronicleThreat);out.writeInt(w.chronicleTension);out.writeInt(w.chronicleNextDay);
        List<ChronicleHistory.Entry> entries=w.chronicleHistory==null?Collections.emptyList():w.chronicleHistory.entries();if(entries.size()>1000)throw new IOException("Chronicle history is too large");out.writeInt(entries.size());
        for(ChronicleHistory.Entry entry:entries){out.writeInt(entry.day);writeString(out,entry.incidentId);out.writeLong(entry.selectionSeed);out.writeInt(entry.tensionAfter);writeString(out,entry.headline);writeString(out,entry.story);writeString(out,entry.tone);writeString(out,entry.source);}
    }
    private static GameWorld readV4(DataInputStream in)throws IOException,SaveException{
        GameWorld w=readV3(in);int threat=in.readInt(),tension=in.readInt(),nextDay=in.readInt();if(threat<0||threat>100||tension<0||tension>100||nextDay<1||nextDay>1_000_000)throw new SaveException("Invalid Chronicle state");
        int n=count(in,"Chronicle history",1000);List<ChronicleHistory.Entry> entries=new ArrayList<>();for(int i=0;i<n;i++){int eventDay=in.readInt();String incident=readString(in);long seed=in.readLong();int after=in.readInt();String headline=readString(in),story=readString(in),tone=readString(in),source=readString(in);if(eventDay<1||eventDay>1_000_000||after<0||after>100)throw new SaveException("Invalid Chronicle entry");try{ChronicleDirector.Incident.valueOf(incident.toUpperCase(Locale.US).replace('-','_'));entries.add(new ChronicleHistory.Entry(eventDay,incident,seed,after,headline,story,tone,source));}catch(IllegalArgumentException e){throw new SaveException("Invalid Chronicle incident",e);}}
        w.chronicleThreat=threat;w.chronicleTension=tension;w.chronicleNextDay=nextDay;w.chronicleHistory=new ChronicleHistory(entries);return w;
    }
    private static GameWorld copyWithSeed(GameWorld source,long seed){
        GameWorld w=new GameWorld(seed,false);w.random.state=source.random.state;w.wood=source.wood;w.stone=source.stone;w.food=source.food;w.crystal=source.crystal;w.components=source.components;w.day=source.day;w.timeOfDay=source.timeOfDay;w.beaconCharge=source.beaconCharge;w.messageTime=source.messageTime;w.bondCharge=source.bondCharge;w.bondActiveSeconds=source.bondActiveSeconds;w.paused=source.paused;w.speed=source.speed;w.tool=source.tool;w.result=source.result;w.raidSpawned=source.raidSpawned;w.message=source.message;w.nextEntityId=source.nextEntityId;
        for(int x=0;x<GameWorld.WIDTH;x++)for(int y=0;y<GameWorld.HEIGHT;y++){w.tiles[x][y]=source.tiles[x][y];w.walls[x][y]=source.walls[x][y];}w.colonists.addAll(source.colonists);w.enemies.addAll(source.enemies);w.blueprints.addAll(source.blueprints);w.stockpiles.addAll(source.stockpiles);w.resourcePiles.addAll(source.resourcePiles);w.productionJobs.addAll(source.productionJobs);return w;
    }

    private static void writeColonistV2(DataOutputStream out,GameWorld.Colonist c)throws IOException{
        writeString(out,c.id);writeString(out,c.name);out.writeFloat(c.x);out.writeFloat(c.y);out.writeFloat(c.health);out.writeFloat(c.hunger);out.writeFloat(c.energy);out.writeFloat(c.mood);out.writeFloat(c.comfort);out.writeFloat(c.safety);out.writeInt(c.tx);out.writeInt(c.ty);out.writeFloat(c.work);out.writeBoolean(c.drafted);out.writeBoolean(c.resting);out.writeByte(c.order.ordinal());out.writeLong(c.attackTargetId);out.writeLong(c.assignedObjectId);out.writeByte(c.assignedWork==null?255:c.assignedWork.ordinal());out.writeLong(enumMask(c.traits));
        for(Skill s:Skill.values())out.writeByte(c.skill(s));for(WorkType w:WorkType.values())out.writeByte(c.priority(w));
        out.writeInt(c.thoughts.size());for(Thought t:c.thoughts){writeString(out,t.label);out.writeFloat(t.moodEffect);out.writeFloat(t.secondsLeft);}out.writeInt(c.injuries.size());for(Injury i:c.injuries){writeString(out,i.label);out.writeFloat(i.severity);out.writeFloat(i.bleedPerSecond);}
    }
    private static GameWorld.Colonist readColonistV2(DataInputStream in)throws IOException,SaveException{
        String id=readString(in),name=readString(in);GameWorld.Colonist c=new GameWorld.Colonist(id,name,in.readFloat(),in.readFloat());c.health=in.readFloat();c.hunger=in.readFloat();c.energy=in.readFloat();c.mood=in.readFloat();c.comfort=in.readFloat();c.safety=in.readFloat();c.tx=in.readInt();c.ty=in.readInt();c.work=in.readFloat();c.drafted=in.readBoolean();c.resting=in.readBoolean();c.order=enumAt(OrderType.values(),in.readUnsignedByte(),"order");c.attackTargetId=in.readLong();c.assignedObjectId=in.readLong();int assigned=in.readUnsignedByte();c.assignedWork=assigned==255?null:enumAt(WorkType.values(),assigned,"assigned work");c.traits.addAll(mask(in.readLong(),Trait.class));
        for(Skill s:Skill.values())c.setSkill(s,in.readUnsignedByte());for(WorkType w:WorkType.values())c.setPriority(w,in.readUnsignedByte());
        int thoughts=count(in,"thoughts",1_000);for(int i=0;i<thoughts;i++)c.thoughts.add(new Thought(readString(in),in.readFloat(),in.readFloat()));int injuries=count(in,"injuries",1_000);for(int i=0;i<injuries;i++)c.injuries.add(new Injury(readString(in),in.readFloat(),in.readFloat()));return c;
    }
    private static void applyMigrationIdentity(GameWorld.Colonist c,int index){Trait[] traits={Trait.KINDLED_SPIRIT,Trait.STEADY_HANDS,Trait.STAR_LISTENER};Skill[] skills={Skill.MEDIC,Skill.BUILD,Skill.CRAFT};c.traits.add(traits[Math.min(index,2)]);c.setSkill(skills[Math.min(index,2)],index==0?5:6);c.setSkill(Skill.HAUL,3);}
    private static byte[] frame(byte[] payload,int version)throws IOException{CRC32 crc=new CRC32();crc.update(payload);ByteArrayOutputStream all=new ByteArrayOutputStream();DataOutputStream out=new DataOutputStream(all);out.writeInt(MAGIC);out.writeInt(version);out.writeInt(payload.length);out.write(payload);out.writeInt((int)crc.getValue());out.flush();return all.toByteArray();}
    private static long enumMask(Collection<? extends Enum<?>> values){long mask=0;for(Enum<?> value:values)mask|=1L<<value.ordinal();return mask;}
    private static <E extends Enum<E>>EnumSet<E> mask(long bits,Class<E> type){EnumSet<E> out=EnumSet.noneOf(type);for(E e:type.getEnumConstants())if((bits&(1L<<e.ordinal()))!=0)out.add(e);return out;}
    private static int count(DataInputStream in,String label,int max)throws IOException,SaveException{int n=in.readInt();if(n<0||n>max)throw new SaveException("Invalid "+label+" count");return n;}
    private static <T>T enumAt(T[] values,int index,String label)throws SaveException{if(index<0||index>=values.length)throw new SaveException("Invalid "+label);return values[index];}
    private static void writeString(DataOutputStream out,String value)throws IOException{byte[] b=(value==null?"":value).getBytes(StandardCharsets.UTF_8);if(b.length>65535)throw new IOException("String too long");out.writeShort(b.length);out.write(b);}
    private static String readString(DataInputStream in)throws IOException,SaveException{int n=in.readUnsignedShort();if(n>4096)throw new SaveException("String too long");byte[] b=new byte[n];in.readFully(b);return new String(b,StandardCharsets.UTF_8);}
    private static void writeNullable(DataOutputStream out,String value)throws IOException{out.writeBoolean(value!=null);if(value!=null)writeString(out,value);}
    private static String readNullable(DataInputStream in)throws IOException,SaveException{return in.readBoolean()?readString(in):null;}
}
