/**
 * Serializer for modifying save game files
 * Uses a "patch in place" approach - copies the original buffer and modifies specific bytes
 */
import { SaveGameParser } from './SaveGameParser.js';
import { BinaryWriter } from './BinaryWriter.js';
import { GameStateTypes, GameStateSizes, Actor, Act1TextureOrder } from '../savegame/constants.js';
import { CharacterFieldOffsets, CHARACTER_RECORD_SIZE } from '../savegame/actorConstants.js';
import { PlantFieldOffsets, PLANT_RECORD_SIZE } from '../savegame/plantConstants.js';
import { BuildingFieldOffsets, BUILDING_RECORD_SIZE } from '../savegame/buildingConstants.js';

/**
 * Offsets for header fields
 */
const HEADER_OFFSETS = {
    VERSION: 0,      // S32, 4 bytes
    PLAYER_ID: 4,    // S16, 2 bytes
    CURRENT_ACT: 6,  // U16, 2 bytes
    ACTOR_ID: 8      // U8, 1 byte
};

/**
 * Map mission type to state name
 */
const MISSION_STATE_MAP = {
    pizza: GameStateTypes.PIZZA_MISSION,
    carRace: GameStateTypes.CAR_RACE,
    jetskiRace: GameStateTypes.JETSKI_RACE,
    towTrack: GameStateTypes.TOW_TRACK,
    ambulance: GameStateTypes.AMBULANCE
};

/**
 * Serializer for modifying save game files
 */
export class SaveGameSerializer {
    /**
     * @param {ArrayBuffer} originalBuffer - Original file data
     */
    constructor(originalBuffer) {
        this.original = originalBuffer;
        // Parse to find state locations
        this.parser = new SaveGameParser(originalBuffer);
        this.parsed = this.parser.parse();
    }

    /**
     * Create a copy of the original buffer for modification
     * @returns {ArrayBuffer}
     */
    createCopy() {
        return this.original.slice(0);
    }

    /**
     * Update header fields (act and actor)
     * @param {Object} updates - Fields to update
     * @param {number} [updates.currentAct] - New act (0-2)
     * @param {number} [updates.actorId] - New actor ID (1-5)
     * @returns {ArrayBuffer} - Modified buffer
     */
    updateHeader(updates) {
        const copy = this.createCopy();
        const view = new DataView(copy);

        if ('currentAct' in updates && updates.currentAct !== undefined) {
            view.setUint16(HEADER_OFFSETS.CURRENT_ACT, updates.currentAct, true);
        }

        if ('actorId' in updates && updates.actorId !== undefined) {
            view.setUint8(HEADER_OFFSETS.ACTOR_ID, updates.actorId);
        }

        return copy;
    }

    /**
     * Create default state data for a mission type
     * @param {string} stateName - State name (e.g., 'CarRaceState')
     * @returns {Uint8Array} - State data with default values (all zeros)
     */
    createDefaultStateData(stateName) {
        const size = GameStateSizes[stateName];
        if (!size) {
            throw new Error(`Unknown state size for: ${stateName}`);
        }

        const data = new Uint8Array(size);

        // For race states, we need to set the actor IDs
        if (stateName === GameStateTypes.CAR_RACE || stateName === GameStateTypes.JETSKI_RACE) {
            // 5 entries of 5 bytes: id(1) + score(2) + hiScore(2)
            for (let i = 0; i < 5; i++) {
                data[i * 5] = Actor.PEPPER + i; // Set actor ID
            }
        }

        return data;
    }

    /**
     * Add a missing state to the save file
     * @param {ArrayBuffer} buffer - Current buffer
     * @param {string} stateName - State name to add
     * @returns {ArrayBuffer} - New buffer with state added
     */
    addMissingState(buffer, stateName) {
        const stateData = this.createDefaultStateData(stateName);
        const nameBytes = new TextEncoder().encode(stateName);

        // Calculate new state size: name length (2) + name + data
        const stateSize = 2 + nameBytes.length + stateData.length;

        // Insert position is where states end (before previous_area field)
        const insertOffset = this.parsed.statesEndOffset;

        // Create new buffer with space for the new state
        const newBuffer = new ArrayBuffer(buffer.byteLength + stateSize);
        const newView = new DataView(newBuffer);
        const newArray = new Uint8Array(newBuffer);
        const oldArray = new Uint8Array(buffer);

        // Copy everything up to insert point
        newArray.set(oldArray.slice(0, insertOffset));

        // Write new state at insert point
        let offset = insertOffset;

        // Write name length (S16)
        newView.setInt16(offset, nameBytes.length, true);
        offset += 2;

        // Write name
        newArray.set(nameBytes, offset);
        offset += nameBytes.length;

        // Write state data
        newArray.set(stateData, offset);
        offset += stateData.length;

        // Copy the rest of the original buffer (previous_area field)
        newArray.set(oldArray.slice(insertOffset), offset);

        // Increment state count
        const stateCountOffset = this.parsed.stateCountOffset;
        const oldCount = new DataView(buffer).getInt16(stateCountOffset, true);
        newView.setInt16(stateCountOffset, oldCount + 1, true);

        // Update parsed data to include the new state
        const dataOffset = insertOffset + 2 + nameBytes.length;
        this.parsed.stateLocations.push({
            name: stateName,
            nameOffset: insertOffset,
            dataOffset: dataOffset,
            dataSize: stateData.length
        });
        this.parsed.stateCount++;
        this.parsed.statesEndOffset = insertOffset + stateSize;

        // Update original reference for subsequent operations
        this.original = newBuffer;

        return newBuffer;
    }

    /**
     * Ensure a mission state exists in the buffer, adding it if missing
     * @param {ArrayBuffer} buffer - Current buffer
     * @param {string} missionType - Mission type
     * @returns {ArrayBuffer} - Buffer with state present
     */
    ensureMissionState(buffer, missionType) {
        const stateName = MISSION_STATE_MAP[missionType];
        if (!stateName) {
            throw new Error(`Unknown mission type: ${missionType}`);
        }

        const stateLocation = this.parsed.stateLocations.find(loc => loc.name === stateName);
        if (stateLocation) {
            return buffer; // State already exists
        }

        return this.addMissingState(buffer, stateName);
    }

    /**
     * Update mission scores in the save file
     * @param {string} missionType - Mission type (pizza, carRace, jetskiRace, towTrack, ambulance)
     * @param {number} actorId - Actor ID (1-5)
     * @param {'score'|'highScore'} scoreType - Which score to update
     * @param {number} value - New score color value (0-3)
     * @param {ArrayBuffer} [buffer] - Optional buffer to use (for chaining operations)
     * @returns {ArrayBuffer} - Modified buffer
     */
    updateMissionScore(missionType, actorId, scoreType, value, buffer = null) {
        const stateName = MISSION_STATE_MAP[missionType];
        if (!stateName) {
            console.error(`Unknown mission type: ${missionType}`);
            return null;
        }

        // Use provided buffer or create a copy
        let workingBuffer = buffer || this.createCopy();

        // Ensure the state exists
        workingBuffer = this.ensureMissionState(workingBuffer, missionType);

        const stateLocation = this.parsed.stateLocations.find(loc => loc.name === stateName);
        if (!stateLocation) {
            console.error(`Failed to find or create state: ${stateName}`);
            return null;
        }

        const offset = this.getMissionScoreOffset(missionType, actorId, scoreType);
        if (offset !== null) {
            new DataView(workingBuffer).setInt16(offset, value, true);
        }

        return workingBuffer;
    }

    /**
     * Apply multiple updates at once
     * @param {Object} updates - Updates to apply
     * @param {Object} [updates.header] - Header updates
     * @param {Array} [updates.missions] - Mission score updates
     * @returns {ArrayBuffer} - Modified buffer
     */
    applyUpdates(updates) {
        let buffer = this.original;

        // Apply header updates first
        if (updates.header) {
            const view = new DataView(buffer = buffer.slice(0));

            if ('currentAct' in updates.header) {
                view.setUint16(HEADER_OFFSETS.CURRENT_ACT, updates.header.currentAct, true);
            }
            if ('actorId' in updates.header) {
                view.setUint8(HEADER_OFFSETS.ACTOR_ID, updates.header.actorId);
            }
        }

        // Apply mission updates
        if (updates.missions && updates.missions.length > 0) {
            for (const mission of updates.missions) {
                buffer = this.updateMissionScore(
                    mission.missionType,
                    mission.actorId,
                    mission.scoreType,
                    mission.value,
                    buffer
                );
            }
        }

        return buffer;
    }

    /**
     * Update a variable value in the save file
     * @param {string} name - Variable name
     * @param {string} newValue - New value string
     * @param {ArrayBuffer} [buffer] - Optional buffer to use (for chaining operations)
     * @returns {ArrayBuffer|null} - Modified buffer or null on error
     */
    updateVariable(name, newValue, buffer = null) {
        const varInfo = this.parsed.variables.get(name);
        if (!varInfo) {
            console.error(`Variable not found: ${name}`);
            return null;
        }

        const workingBuffer = buffer || this.createCopy();
        const array = new Uint8Array(workingBuffer);
        const encoder = new TextEncoder();

        const oldLength = varInfo.valueLength;
        const newLength = newValue.length;

        if (oldLength === newLength) {
            // In-place update - same length, just replace bytes
            const valueBytes = encoder.encode(newValue);
            array.set(valueBytes, varInfo.valueOffset + 1); // +1 for length byte
            return workingBuffer;
        } else {
            // Different length - need to rebuild buffer
            const oldArray = new Uint8Array(workingBuffer);
            const sizeDiff = newLength - oldLength;
            const newBuffer = new ArrayBuffer(workingBuffer.byteLength + sizeDiff);
            const newArray = new Uint8Array(newBuffer);

            // Copy everything up to the value (including the length byte position)
            const valueDataStart = varInfo.valueOffset + 1; // After length byte
            newArray.set(oldArray.slice(0, varInfo.valueOffset));

            // Write new length byte
            newArray[varInfo.valueOffset] = newLength;

            // Write new value
            const valueBytes = encoder.encode(newValue);
            newArray.set(valueBytes, valueDataStart);

            // Copy everything after the old value
            const afterOldValue = valueDataStart + oldLength;
            newArray.set(oldArray.slice(afterOldValue), valueDataStart + newLength);

            return newBuffer;
        }
    }

    /**
     * Update an Act1State texture in the save file
     * @param {string} textureName - Texture name (e.g. 'chwind.gif')
     * @param {{ palette: Array<{r,g,b}>, pixels: Uint8Array, width: number, height: number }} newTextureData
     * @param {ArrayBuffer} [buffer] - Optional buffer to use
     * @returns {ArrayBuffer|null} - Modified buffer or null on error
     */
    updateAct1Texture(textureName, newTextureData, buffer = null) {
        const workingBuffer = buffer || this.createCopy();

        // Re-parse to get fresh Act1State from the working buffer
        const freshParser = new SaveGameParser(workingBuffer);
        const freshParsed = freshParser.parse();
        const act1State = freshParsed.act1State;

        if (!act1State) {
            console.error('Act1State not found in save file');
            return null;
        }

        const act1Location = freshParsed.stateLocations.find(loc => loc.name === 'Act1State');
        if (!act1Location) {
            console.error('Act1State location not found');
            return null;
        }

        const targetKey = textureName.toLowerCase();
        if (!act1State.textures.has(targetKey)) {
            console.error(`Texture not found in Act1State: ${textureName}`);
            return null;
        }

        // Replace texture data, preserving original name
        const oldTex = act1State.textures.get(targetKey);
        act1State.textures.set(targetKey, {
            name: oldTex.name,
            width: newTextureData.width,
            height: newTextureData.height,
            paletteSize: newTextureData.palette.length,
            palette: newTextureData.palette,
            pixels: newTextureData.pixels
        });

        return this._rebuildAct1State(workingBuffer, act1Location, act1State);
    }

    /**
     * Rebuild the full buffer with updated Act1State
     * @private
     */
    _rebuildAct1State(sourceBuffer, act1Location, act1State) {
        const writer = new BinaryWriter(sourceBuffer.byteLength + 4096);
        const srcArray = new Uint8Array(sourceBuffer);

        // Write 7 planes
        let readOffset = act1Location.dataOffset;
        for (const plane of act1State.planes) {
            writer.writeS16(plane.nameLength);
            readOffset += 2;
            if (plane.nameLength > 0) {
                writer.writeString(plane.name);
                readOffset += plane.nameLength;
            }
            // Copy 36 bytes of position/direction/up from source
            writer.writeBytes(srcArray.slice(readOffset, readOffset + 36));
            readOffset += 36;
        }

        // Write conditional textures in correct order
        const vehicleOrder = ['helicopter', 'jetski', 'dunebuggy', 'racecar'];
        const planeIndices = [3, 4, 5, 6];

        for (let v = 0; v < vehicleOrder.length; v++) {
            const vehicleName = vehicleOrder[v];
            const planeIdx = planeIndices[v];
            if (act1State.planes[planeIdx].nameLength <= 0) continue;

            const textureNames = Act1TextureOrder[vehicleName];
            for (const texName of textureNames) {
                const texKey = texName.toLowerCase();
                const tex = act1State.textures.get(texKey);
                if (!tex) continue;

                writer.writeS16(tex.name.length);
                writer.writeString(tex.name);
                writer.writeU32(tex.width);
                writer.writeU32(tex.height);
                writer.writeU32(tex.paletteSize);
                for (const color of tex.palette) {
                    writer.writeU8(color.r);
                    writer.writeU8(color.g);
                    writer.writeU8(color.b);
                }
                writer.writeBytes(tex.pixels);
            }
        }

        // Write final fields
        writer.writeS16(act1State.cptClickDialogueNextIndex);
        writer.writeU8(act1State.playedExitExplanation);

        const newAct1Data = writer.toUint8Array();
        const oldAct1Size = act1Location.dataSize;
        const newAct1Size = newAct1Data.length;
        const sizeDiff = newAct1Size - oldAct1Size;

        // Build final buffer
        const newBuffer = new ArrayBuffer(sourceBuffer.byteLength + sizeDiff);
        const newArray = new Uint8Array(newBuffer);

        // Copy everything before Act1State data
        newArray.set(srcArray.slice(0, act1Location.dataOffset));

        // Write new Act1State data
        newArray.set(newAct1Data, act1Location.dataOffset);

        // Copy everything after old Act1State data
        const afterOld = act1Location.dataOffset + oldAct1Size;
        newArray.set(srcArray.slice(afterOld), act1Location.dataOffset + newAct1Size);

        return newBuffer;
    }

    /**
     * Update a character field in the save file
     * @param {number} characterIndex - Character index (0-65)
     * @param {string} field - Field name from CharacterFieldOffsets
     * @param {number} value - New value
     * @returns {ArrayBuffer} - Modified buffer
     */
    updateCharacter(characterIndex, field, value) {
        const workingBuffer = this.createCopy();
        const view = new DataView(workingBuffer);
        const offset = this.parsed.charactersOffset + (characterIndex * CHARACTER_RECORD_SIZE) + CharacterFieldOffsets[field];

        if (field === 'sound' || field === 'move') {
            view.setInt32(offset, value, true);
        } else {
            view.setUint8(offset, value);
        }

        return workingBuffer;
    }

    /**
     * Update a plant field in the save file
     * @param {number} plantIndex - Plant index (0-80)
     * @param {string} field - Field name from PlantFieldOffsets
     * @param {number} value - New value
     * @returns {ArrayBuffer} - Modified buffer
     */
    updatePlant(plantIndex, field, value) {
        const workingBuffer = this.createCopy();
        const view = new DataView(workingBuffer);
        const offset = this.parsed.plantsOffset + (plantIndex * PLANT_RECORD_SIZE) + PlantFieldOffsets[field];

        if (field === 'sound' || field === 'move') {
            view.setUint32(offset, value, true);
        } else if (field === 'counter') {
            view.setInt8(offset, value);
        } else {
            view.setUint8(offset, value);
        }

        return workingBuffer;
    }

    /**
     * Update a building field in the save file
     * @param {number} buildingIndex - Building index (0-15)
     * @param {string} field - Field name from BuildingFieldOffsets
     * @param {number} value - New value
     * @returns {ArrayBuffer} - Modified buffer
     */
    updateBuilding(buildingIndex, field, value) {
        const workingBuffer = this.createCopy();
        const view = new DataView(workingBuffer);
        const offset = this.parsed.buildingsOffset + (buildingIndex * BUILDING_RECORD_SIZE) + BuildingFieldOffsets[field];

        if (field === 'sound' || field === 'move') {
            view.setUint32(offset, value, true);
        } else if (field === 'counter') {
            view.setInt8(offset, value);
        } else {
            view.setUint8(offset, value);
        }

        return workingBuffer;
    }

    /**
     * Update the nextVariant field in the save file
     * @param {number} value - New variant value (0-4)
     * @returns {ArrayBuffer} - Modified buffer
     */
    updateNextVariant(value) {
        const workingBuffer = this.createCopy();
        const view = new DataView(workingBuffer);
        view.setUint8(this.parsed.nextVariantOffset, value);
        return workingBuffer;
    }

    /**
     * Get the byte offset for a mission score
     * @param {string} missionType
     * @param {number} actorId
     * @param {'score'|'highScore'} scoreType
     * @returns {number|null}
     */
    getMissionScoreOffset(missionType, actorId, scoreType) {
        const stateName = MISSION_STATE_MAP[missionType];
        if (!stateName) return null;

        const stateLocation = this.parsed.stateLocations.find(loc => loc.name === stateName);
        if (!stateLocation) return null;

        const actorIndex = actorId - Actor.PEPPER;

        if (missionType === 'pizza') {
            const entryOffset = stateLocation.dataOffset + (actorIndex * 8);
            return scoreType === 'score' ? entryOffset + 4 : entryOffset + 6;
        } else if (missionType === 'carRace' || missionType === 'jetskiRace') {
            const entryOffset = stateLocation.dataOffset + (actorIndex * 5);
            return scoreType === 'score' ? entryOffset + 1 : entryOffset + 3;
        } else if (missionType === 'towTrack' || missionType === 'ambulance') {
            if (scoreType === 'score') {
                return stateLocation.dataOffset + (actorIndex * 2);
            } else {
                return stateLocation.dataOffset + 10 + (actorIndex * 2);
            }
        }

        return null;
    }
}

/**
 * Create a serializer for a save game buffer
 * @param {ArrayBuffer} buffer
 * @returns {SaveGameSerializer}
 */
export function createSerializer(buffer) {
    return new SaveGameSerializer(buffer);
}
