package p.packer.repositories;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

public class PackerAssetWalker {
    private static final Set<OutputFormatCatalog> TILE_BANK_SUPPORTED_FORMATS = Set.of(
            OutputFormatCatalog.TILES_INDEXED_V1
    );

    private static final Set<OutputFormatCatalog> SOUND_BANK_SUPPORTED_FORMATS = Set.of(
            OutputFormatCatalog.SOUND_V1
    );

    private final PackerTileBankWalker tileBankWalker;
    private final PackerSoundBankWalker soundBankWalker;

    public PackerAssetWalker(final ObjectMapper mapper) {
        this.tileBankWalker = new PackerTileBankWalker(mapper);
        this.soundBankWalker = new PackerSoundBankWalker(mapper);
    }

    public PackerWalkResult walk(
            final Path assetRoot,
            final PackerAssetDeclaration declaration) {
        return walk(assetRoot, declaration, Optional.empty());
    }

    public PackerWalkResult walk(
            final Path assetRoot,
            final PackerAssetDeclaration declaration,
            final Optional<PackerAssetCacheEntry> priorAssetCache) {
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        switch (declaration.assetFamily()) {
            case TILE_BANK -> {
                var metadata = declaration.outputMetadata();
                if (MapUtils.isEmpty(metadata)) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.WARNING,
                            PackerDiagnosticCategory.HYGIENE,
                            "Output metadata for tile bank cannot be empty, using default values",
                            assetRoot,
                            false));
                    metadata = Map.of("tile_size", "16x16");
                }
                final var requirementBuildResult = buildTileBankRequirements(declaration, metadata);
                if (requirementBuildResult.hasError()) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.ERROR,
                            PackerDiagnosticCategory.STRUCTURAL,
                            requirementBuildResult.errorMessage(),
                            assetRoot,
                            true));
                    return new PackerWalkResult(List.of(), diagnostics);
                }
                final var walkResult = tileBankWalker.walk(assetRoot, requirementBuildResult.requirements, priorAssetCache);
                diagnostics.addAll(walkResult.diagnostics());
                return new PackerWalkResult(walkResult.probeResults(), diagnostics);
            }
            case SOUND_BANK -> {
                var metadata = declaration.outputMetadata();
                if (MapUtils.isEmpty(metadata)) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.WARNING,
                            PackerDiagnosticCategory.HYGIENE,
                            "Output metadata for tile bank cannot be empty, using default values",
                            assetRoot,
                            false));
                    metadata = Map.of("sample_rate", "44100", "channels", "1");
                }
                final var result = buildSoundBankRequirements(declaration, metadata);
                if (result.hasError()) {
                    diagnostics.add(new PackerDiagnostic(
                            PackerDiagnosticSeverity.ERROR,
                            PackerDiagnosticCategory.STRUCTURAL,
                            result.errorMessage(),
                            assetRoot,
                            true));
                    return new PackerWalkResult(List.of(), diagnostics);
                }
                final var walkResult = soundBankWalker.walk(assetRoot, result.requirements, priorAssetCache);
                diagnostics.addAll(walkResult.diagnostics());
                return new PackerWalkResult(walkResult.probeResults(), diagnostics);
            }
            case UNKNOWN -> {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.WARNING,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Unknown asset family for declaration, skipping content walk",
                        assetRoot,
                        false));
                return new PackerWalkResult(List.of(), diagnostics);
            }
        }
        return PackerWalkResult.EMPTY;
    }

    private RequirementBuildResult<PackerTileBankRequirements> buildTileBankRequirements(
            final PackerAssetDeclaration declaration,
            final Map<String, String> metadata) {
        if (!TILE_BANK_SUPPORTED_FORMATS.contains(declaration.outputFormat())) {
            return RequirementBuildResult.fail("Unsupported output format for tile bank: " + declaration.outputFormat());
        }
        final var tileSizeStr = metadata.get("tile_size");
        if (StringUtils.isBlank(tileSizeStr)) {
            return RequirementBuildResult.fail("Tile size metadata for tile bank cannot be empty");
        }
        final int tileSize;
        switch (tileSizeStr) {
            case "8x8": {
                tileSize = 8;
            } break;
            case "16x16": {
                tileSize = 16;
            } break;
            case "32x32": {
                tileSize = 32;
            } break;
            default: {
                return RequirementBuildResult.fail("Unsupported tile size for tile bank: " + tileSizeStr);
            }
        }
        return RequirementBuildResult.success(new PackerTileBankRequirements(tileSize));
    }

    private RequirementBuildResult<PackerSoundBankRequirements> buildSoundBankRequirements(
            final PackerAssetDeclaration declaration,
            final Map<String, String> metadata) {
        if (!SOUND_BANK_SUPPORTED_FORMATS.contains(declaration.outputFormat())) {
            return RequirementBuildResult.fail("Unsupported output format for sound bank: " + declaration.outputFormat());
        }
        final var sampleRateStr = metadata.get("sample_rate");
        if (StringUtils.isBlank(sampleRateStr)) {
            return RequirementBuildResult.fail("Missing sample rate for sound bank");
        }
        final var sampleRate = Integer.parseInt(sampleRateStr);
        final var channelsStr = metadata.get("channels");
        if (StringUtils.isBlank(channelsStr)) {
            return RequirementBuildResult.fail("Missing channels for sound bank");
        }
        final var channels = Integer.parseInt(channelsStr);
        return RequirementBuildResult.success(new PackerSoundBankRequirements(sampleRate, channels));
    }

    private record RequirementBuildResult<T>(
            T requirements,
            String errorMessage) {
        static <T> RequirementBuildResult<T> success(T requirements) {
            return new RequirementBuildResult<>(requirements, null);
        }

        static <T> RequirementBuildResult<T> fail(String errorMessage) {
            return new RequirementBuildResult<>(null, errorMessage);
        }

        public boolean hasError() {
            return StringUtils.isNotBlank(errorMessage);
        }
    }
}
