package p.studio.workspaces.assets.tiled;

import p.packer.dtos.PackerCodecConfigurationFieldDTO;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public final class TiledAssetGenerationService {
    public static final String GENERATED_TSX_FILE = "tileset.tsx";

    private static final long FLIP_HORIZONTAL_FLAG = 0x80000000L;
    private static final long FLIP_VERTICAL_FLAG = 0x40000000L;
    private static final long FLIP_DIAGONAL_FLAG = 0x20000000L;
    private static final long GID_MASK = ~(FLIP_HORIZONTAL_FLAG | FLIP_VERTICAL_FLAG | FLIP_DIAGONAL_FLAG);

    private final TiledXmlCodec codec = new TiledXmlCodec();
    private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();

    public enum SceneTmxExportDisposition {
        OVERWRITE,
        KEEP_BOTH
    }

    public TiledAssetGenerationResult generateTilesetTsx(AssetWorkspaceAssetDetails details) {
        return exportTilesetTsx(details);
    }

    public TiledAssetGenerationResult exportTilesetTsx(AssetWorkspaceAssetDetails details) {
        if (details == null
                || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK
                || details.summary().glyphSpecialization() != AssetStudioGlyphSpecialization.TILESET) {
            return new TiledAssetGenerationResult(false, "TSX export is available only for Tileset-specialized glyph banks.", List.of(), List.of());
        }
        final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
        if (selectedFiles.isEmpty()) {
            return new TiledAssetGenerationResult(false, "The selected tileset bank has no included glyph artifacts.", List.of(), List.of());
        }
        final int tileSize = parseTileSize(details.metadataFields());
        final List<TiledTilesetTile> tiles = new ArrayList<>();
        for (int index = 0; index < selectedFiles.size(); index += 1) {
            final AssetWorkspaceBankCompositionFile file = selectedFiles.get(index);
            tiles.add(new TiledTilesetTile(
                    index,
                    file.path(),
                    tileSize,
                    tileSize,
                    List.of(new TiledProperty("glyph_id", "int", Integer.toString(index))),
                    null));
        }
        final TiledTilesetDocument document = new TiledTilesetDocument(
                "1.10",
                "1.12.1",
                details.summary().assetName(),
                tileSize,
                tileSize,
                tiles.size(),
                0,
                List.of(),
                tiles);
        final Path outputPath = details.summary().assetRoot().resolve(GENERATED_TSX_FILE);
        try {
            codec.writeTileset(outputPath, document);
            return new TiledAssetGenerationResult(true, "Exported TSX successfully.", List.of(), List.of(outputPath));
        } catch (IOException exception) {
            return new TiledAssetGenerationResult(false, "Unable to export TSX: " + exception.getMessage(), List.of(), List.of());
        }
    }

    public TiledAssetGenerationResult generateSceneBankTilemaps(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        return exportSceneBankTmx(projectReference, details);
    }

    public TiledAssetGenerationResult generateSceneBankTilemap(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
        return exportSceneBankTmx(projectReference, details);
    }

    public TiledAssetGenerationResult exportSceneBankTmx(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        return exportSceneBankTmx(projectReference, details, SceneTmxExportDisposition.OVERWRITE);
    }

    public TiledAssetGenerationResult exportSceneBankTmx(
            ProjectReference projectReference,
            AssetWorkspaceAssetDetails details,
            SceneTmxExportDisposition disposition) {
        if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
            return new TiledAssetGenerationResult(false, "TMX export is available only for Scene Bank assets.", List.of(), List.of());
        }
        final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
        if (metadata == null) {
            return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of(), List.of());
        }
        final List<AssetStudioSceneLayerBinding> enabledLayers = metadata.layerBindings().stream()
                .filter(AssetStudioSceneLayerBinding::enabled)
                .toList();
        if (enabledLayers.isEmpty()) {
            return new TiledAssetGenerationResult(false, "Enable at least one layer before exporting TMX.", List.of(), List.of());
        }
        final String sceneTilemap = resolveSceneTilemap(metadata);
        if (sceneTilemap == null) {
            return new TiledAssetGenerationResult(false, "Scene TMX export requires a single shared tilemap binding across Scene Bank layers.", List.of(), List.of());
        }
        try {
            final ExportContext context = buildExportContext(projectReference, details, enabledLayers);
            final List<TiledTileLayer> tileLayers = new ArrayList<>();
            int nextLayerId = 1;
            for (AssetStudioSceneLayerBinding binding : enabledLayers) {
                final TilesetRef ref = context.tilesetByAssetRoot().get(binding.tilesetAssetRoot());
                final List<Long> gids = new ArrayList<>(binding.tiles().size());
                for (AssetStudioSceneTile tile : binding.tiles()) {
                    if (!tile.active()) {
                        gids.add(0L);
                        continue;
                    }
                    if (tile.glyphId() >= ref.tileCount()) {
                        return new TiledAssetGenerationResult(
                                false,
                                "Layer " + binding.index() + " references glyph_id outside the exported TSX range.",
                                List.of(),
                                List.of());
                    }
                    long gid = ref.firstGid() + tile.glyphId();
                    if (tile.flipX()) {
                        gid |= FLIP_HORIZONTAL_FLAG;
                    }
                    if (tile.flipY()) {
                        gid |= FLIP_VERTICAL_FLAG;
                    }
                    gids.add(gid);
                }
                tileLayers.add(new TiledTileLayer(
                        nextLayerId,
                        binding.layerName(),
                        binding.width(),
                        binding.height(),
                        gids,
                        List.of()));
                nextLayerId += 1;
            }
            final TiledMapDocument document = new TiledMapDocument(
                    "1.10",
                    "1.12.1",
                    "orthogonal",
                    "right-down",
                    enabledLayers.stream().mapToInt(AssetStudioSceneLayerBinding::width).max().orElse(1),
                    enabledLayers.stream().mapToInt(AssetStudioSceneLayerBinding::height).max().orElse(1),
                    context.tileSize(),
                    context.tileSize(),
                    nextLayerId,
                    1,
                    List.of(),
                    context.tilesetRefs(),
                    tileLayers,
                    List.of());
            final Path outputPath = details.summary().assetRoot().resolve(sceneTilemap);
            final ArrayList<Path> writtenFiles = new ArrayList<>();
            if (disposition == SceneTmxExportDisposition.KEEP_BOTH && Files.isRegularFile(outputPath)) {
                final Path backupPath = nextSceneTmxBackupPath(outputPath);
                Files.copy(outputPath, backupPath);
                writtenFiles.add(backupPath);
            }
            codec.writeMap(outputPath, document);
            writtenFiles.add(outputPath);
            return new TiledAssetGenerationResult(true, "Exported TMX successfully.", enabledLayers, List.copyOf(writtenFiles));
        } catch (IOException | TiledUnsupportedFeatureException exception) {
            return new TiledAssetGenerationResult(false, "Unable to export TMX: " + exception.getMessage(), List.of(), List.of());
        }
    }

    public TiledAssetGenerationResult importSceneBankTmx(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
            return new TiledAssetGenerationResult(false, "TMX import is available only for Scene Bank assets.", List.of(), List.of());
        }
        final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
        if (metadata == null) {
            return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of(), List.of());
        }
        final String sceneTilemap = resolveSceneTilemap(metadata);
        if (sceneTilemap == null) {
            return new TiledAssetGenerationResult(false, "Scene TMX import requires a single shared tilemap binding across Scene Bank layers.", List.of(), List.of());
        }
        final Path inputPath = details.summary().assetRoot().resolve(sceneTilemap);
        if (!Files.isRegularFile(inputPath)) {
            return new TiledAssetGenerationResult(false, "TMX import source is missing: " + sceneTilemap, List.of(), List.of());
        }
        try {
            final List<AssetStudioSceneLayerBinding> enabledLayers = metadata.layerBindings().stream()
                    .filter(AssetStudioSceneLayerBinding::enabled)
                    .toList();
            final ExportContext context = buildExportContext(projectReference, details, enabledLayers);
            final TiledMapDocument map = codec.readMap(inputPath);
            if (map.tileLayers().isEmpty()) {
                return new TiledAssetGenerationResult(false, "TMX import rejected: at least one tile layer is required.", List.of(), List.of());
            }
            if (map.tileLayers().size() > metadata.layerCount()) {
                return new TiledAssetGenerationResult(false, "TMX import rejected: more than four tile layers are not supported.", List.of(), List.of());
            }
            if (map.tilesets().size() != context.tilesetRefs().size()) {
                return new TiledAssetGenerationResult(false, "TMX import rejected: unexpected tileset count.", List.of(), List.of());
            }
            for (int index = 0; index < context.tilesetRefs().size(); index += 1) {
                if (!context.tilesetRefs().get(index).source().equals(map.tilesets().get(index).source())) {
                    return new TiledAssetGenerationResult(false, "TMX import rejected: unexpected tileset reference.", List.of(), List.of());
                }
            }

            final List<AssetStudioSceneLayerBinding> updatedBindings = new ArrayList<>();
            for (int bindingIndex = 0; bindingIndex < metadata.layerBindings().size(); bindingIndex += 1) {
                final AssetStudioSceneLayerBinding binding = metadata.layerBindings().get(bindingIndex);
                if (bindingIndex >= map.tileLayers().size()) {
                    updatedBindings.add(new AssetStudioSceneLayerBinding(
                            false,
                            binding.index(),
                            binding.layerName(),
                            binding.width(),
                            binding.height(),
                            binding.tilemap(),
                            binding.tilesetAssetRoot(),
                            binding.parallaxFactorX(),
                            binding.parallaxFactorY(),
                            binding.defaultPaletteId(),
                            binding.tiles()));
                    continue;
                }
                final TiledTileLayer importedLayer = map.tileLayers().get(bindingIndex);
                if (importedLayer.gids().size() != importedLayer.width() * importedLayer.height()) {
                    return new TiledAssetGenerationResult(false, "TMX import rejected: layer tile data does not match layer dimensions.", List.of(), List.of());
                }
                final TilesetRef ref = resolveImportedLayerTilesetRef(importedLayer, binding, context);
                if (ref == null) {
                    return new TiledAssetGenerationResult(false, "TMX import rejected: layer uses an unexpected tileset.", List.of(), List.of());
                }
                final List<AssetStudioSceneTile> importedTiles = new ArrayList<>(importedLayer.width() * importedLayer.height());
                for (long rawGid : importedLayer.gids()) {
                    if ((rawGid & FLIP_DIAGONAL_FLAG) != 0L) {
                        return new TiledAssetGenerationResult(false, "TMX import rejected: diagonal flips are not supported.", List.of(), List.of());
                    }
                    final boolean flipX = (rawGid & FLIP_HORIZONTAL_FLAG) != 0L;
                    final boolean flipY = (rawGid & FLIP_VERTICAL_FLAG) != 0L;
                    final long gid = rawGid & GID_MASK;
                    if (gid == 0L) {
                        importedTiles.add(AssetStudioSceneTile.inactive(binding.defaultPaletteId()));
                        continue;
                    }
                    if (gid < ref.firstGid() || gid >= ref.firstGid() + ref.tileCount()) {
                        return new TiledAssetGenerationResult(false, "TMX import rejected: tile uses an unexpected tileset.", List.of(), List.of());
                    }
                    final int tilesetTileId = (int) (gid - ref.firstGid());
                    final Integer glyphId = ref.glyphIdByTileId().get(tilesetTileId);
                    if (glyphId == null) {
                        return new TiledAssetGenerationResult(false, "TMX import rejected: tileset tile is missing glyph_id mapping.", List.of(), List.of());
                    }
                    importedTiles.add(new AssetStudioSceneTile(
                            true,
                            glyphId,
                            binding.defaultPaletteId(),
                            flipX,
                            flipY));
                }
                updatedBindings.add(new AssetStudioSceneLayerBinding(
                        true,
                        binding.index(),
                        binding.layerName(),
                        importedLayer.width(),
                        importedLayer.height(),
                        binding.tilemap(),
                        ref.assetRoot(),
                        binding.parallaxFactorX(),
                        binding.parallaxFactorY(),
                        binding.defaultPaletteId(),
                        importedTiles));
            }
            final int mapWidth = Math.max(map.width(), updatedBindings.stream()
                    .filter(AssetStudioSceneLayerBinding::enabled)
                    .mapToInt(AssetStudioSceneLayerBinding::width)
                    .max()
                    .orElse(metadata.mapWidth()));
            final int mapHeight = Math.max(map.height(), updatedBindings.stream()
                    .filter(AssetStudioSceneLayerBinding::enabled)
                    .mapToInt(AssetStudioSceneLayerBinding::height)
                    .max()
                    .orElse(metadata.mapHeight()));
            metadataService.writeSceneBankMetadata(details.summary().assetRoot(), new AssetStudioSceneBankMetadata(
                    mapWidth,
                    mapHeight,
                    metadata.layerCount(),
                    updatedBindings,
                    metadata.supportFile()));
            return new TiledAssetGenerationResult(true, "Imported TMX successfully.", updatedBindings, List.of(inputPath, metadata.supportFile()));
        } catch (IOException | TiledUnsupportedFeatureException exception) {
            return new TiledAssetGenerationResult(false, "Unable to import TMX: " + exception.getMessage(), List.of(), List.of());
        }
    }

    private TilesetRef resolveImportedLayerTilesetRef(
            TiledTileLayer importedLayer,
            AssetStudioSceneLayerBinding binding,
            ExportContext context) {
        TilesetRef resolved = null;
        for (long rawGid : importedLayer.gids()) {
            final long gid = rawGid & GID_MASK;
            if (gid == 0L) {
                continue;
            }
            final TilesetRef ref = context.tilesetByAssetRoot().values().stream()
                    .filter(candidate -> gid >= candidate.firstGid() && gid < candidate.firstGid() + candidate.tileCount())
                    .findFirst()
                    .orElse(null);
            if (ref == null) {
                return null;
            }
            if (resolved != null && !resolved.assetRoot().equals(ref.assetRoot())) {
                return null;
            }
            resolved = ref;
        }
        if (resolved != null) {
            return resolved;
        }
        final String declaredAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
        if (!declaredAssetRoot.isBlank()) {
            return context.tilesetByAssetRoot().get(declaredAssetRoot);
        }
        if (context.tilesetByAssetRoot().size() == 1) {
            return context.tilesetByAssetRoot().values().iterator().next();
        }
        return null;
    }

    private ExportContext buildExportContext(
            ProjectReference projectReference,
            AssetWorkspaceAssetDetails details,
            List<AssetStudioSceneLayerBinding> enabledLayers) throws IOException {
        final LinkedHashMap<String, TilesetRef> refs = new LinkedHashMap<>();
        final List<TiledTilesetReference> tilesetRefs = new ArrayList<>();
        int tileSize = -1;
        int nextFirstGid = 1;
        for (AssetStudioSceneLayerBinding binding : enabledLayers) {
            if (refs.containsKey(binding.tilesetAssetRoot())) {
                continue;
            }
            final Path tilesetPath = projectReference.rootPath()
                    .resolve("assets")
                    .resolve(binding.tilesetAssetRoot())
                    .resolve(GENERATED_TSX_FILE)
                    .toAbsolutePath()
                    .normalize();
            if (!Files.isRegularFile(tilesetPath)) {
                throw new IOException("Referenced TSX was not found for layer " + binding.index() + ": " + binding.tilesetAssetRoot());
            }
            final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
            if (tileSize < 0) {
                tileSize = tileset.tileWidth();
            } else if (tileSize != tileset.tileWidth() || tileSize != tileset.tileHeight()) {
                throw new IOException("All exported Scene Bank tilesets must share the same tile size.");
            }
            final String relativeTilesetPath = details.summary().assetRoot()
                    .toAbsolutePath()
                    .normalize()
                    .relativize(tilesetPath)
                    .toString()
                    .replace('\\', '/');
            refs.put(binding.tilesetAssetRoot(), new TilesetRef(
                    nextFirstGid,
                    binding.tilesetAssetRoot(),
                    relativeTilesetPath,
                    tileset.tileCount(),
                    glyphIdByTileId(tileset)));
            tilesetRefs.add(new TiledTilesetReference(nextFirstGid, relativeTilesetPath));
            nextFirstGid += tileset.tileCount();
        }
        if (tileSize <= 0) {
            throw new IOException("Unable to resolve tile size for Scene Bank export.");
        }
        return new ExportContext(tileSize, List.copyOf(tilesetRefs), Map.copyOf(refs));
    }

    private String resolveSceneTilemap(AssetStudioSceneBankMetadata metadata) {
        final LinkedHashSet<String> tilemaps = new LinkedHashSet<>();
        for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
            final String tilemap = Objects.requireNonNullElse(binding.tilemap(), "").trim();
            if (!tilemap.isBlank()) {
                tilemaps.add(tilemap);
            }
        }
        return tilemaps.size() == 1 ? tilemaps.iterator().next() : null;
    }

    private int parseTileSize(List<PackerCodecConfigurationFieldDTO> metadataFields) {
        final String value = metadataFields.stream()
                .filter(field -> "tile_size".equals(field.key()))
                .map(PackerCodecConfigurationFieldDTO::value)
                .findFirst()
                .orElse("16x16");
        final String normalized = value.trim().toLowerCase();
        if (normalized.startsWith("8x8")) {
            return 8;
        }
        if (normalized.startsWith("32x32")) {
            return 32;
        }
        return 16;
    }

    private Path nextSceneTmxBackupPath(Path outputPath) {
        final String filename = outputPath.getFileName().toString();
        final int extensionOffset = filename.lastIndexOf('.');
        final String stem = extensionOffset >= 0 ? filename.substring(0, extensionOffset) : filename;
        final String extension = extensionOffset >= 0 ? filename.substring(extensionOffset) : "";
        final Path parent = outputPath.getParent();
        int sequence = 1;
        while (true) {
            final Path candidate = parent.resolve(stem + ".bkp-" + sequence + extension);
            if (!Files.exists(candidate)) {
                return candidate;
            }
            sequence += 1;
        }
    }

    private Map<Integer, Integer> glyphIdByTileId(TiledTilesetDocument tileset) throws IOException {
        final LinkedHashMap<Integer, Integer> glyphs = new LinkedHashMap<>();
        for (TiledTilesetTile tile : tileset.tiles()) {
            final Integer glyphId = tile.properties().stream()
                    .filter(property -> "glyph_id".equals(property.name()))
                    .findFirst()
                    .map(TiledProperty::value)
                    .map(String::trim)
                    .filter(value -> !value.isBlank())
                    .map(Integer::parseInt)
                    .orElse(null);
            if (glyphId == null || glyphId < 0) {
                throw new IOException("TSX tile " + tile.id() + " is missing a valid glyph_id property.");
            }
            glyphs.put(tile.id(), glyphId);
        }
        if (glyphs.size() != tileset.tileCount()) {
            throw new IOException("TSX tile count does not match declared tile_count.");
        }
        return Map.copyOf(glyphs);
    }

    private record ExportContext(
            int tileSize,
            List<TiledTilesetReference> tilesetRefs,
            Map<String, TilesetRef> tilesetByAssetRoot) {
    }

    private record TilesetRef(
            int firstGid,
            String assetRoot,
            String source,
            int tileCount,
            Map<Integer, Integer> glyphIdByTileId) {
    }
}
