package org.enso.compiler;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;

import org.apache.commons.lang3.StringUtils;
import org.enso.compiler.data.BindingsMap;
import org.enso.editions.LibraryName;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.pkg.QualifiedName;
import org.enso.pkg.SourceFile;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLogger;

import buildinfo.Info;
import scala.collection.immutable.Map;

public final class ImportExportCache extends Cache<ImportExportCache.CachedBindings, ImportExportCache.Metadata> {

    private final LibraryName libraryName;

    public ImportExportCache(LibraryName libraryName) {
        super(Level.FINEST, libraryName.toString(), true, false);
        this.libraryName = libraryName;
        this.entryName = libraryName.name();
        this.dataSuffix = bindingsCacheDataExtension;
        this.metadataSuffix = bindingsCacheMetadataExtension;
    }

    @Override
    protected byte[] metadata(String sourceDigest, String blobDigest, CachedBindings entry) {
        try {
            return objectMapper.writeValueAsString(new Metadata(sourceDigest, blobDigest)).getBytes(metadataCharset);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected CachedBindings deserialize(EnsoContext context, byte[] data, Metadata meta, TruffleLogger logger) throws ClassNotFoundException, IOException, ClassNotFoundException {
      try (var stream = new ObjectInputStream(new ByteArrayInputStream(data))) {
        if (stream.readObject() instanceof MapToBindings bindings) {
          return new CachedBindings(libraryName, bindings, Optional.empty());
        } else {
          throw new ClassNotFoundException("Expected ImportExportCache.FileToBindings, got " + data.getClass());
        }
      }
    }

    @Override
    protected Optional<Metadata> metadataFromBytes(byte[] bytes, TruffleLogger logger) {
        var maybeJsonString = new String(bytes, Cache.metadataCharset);
        var mapper = new ObjectMapper();
        try {
            return Optional.of(objectMapper.readValue(maybeJsonString, ImportExportCache.Metadata.class));
        } catch (JsonProcessingException e) {
            logger.log(logLevel, "Failed to deserialize library's metadata.", e);
            return Optional.empty();
        }
    }

    @Override
    protected Optional<String> computeDigest(CachedBindings entry, TruffleLogger logger) {
        return entry.sources().map(sources -> computeDigestOfLibrarySources(sources, logger));
    }
    @Override
    @SuppressWarnings("unchecked")
    protected Optional<String> computeDigestFromSource(EnsoContext context, TruffleLogger logger) {
        return context
                .getPackageRepository()
                .getPackageForLibraryJava(libraryName)
                .map(pkg -> computeDigestOfLibrarySources(pkg.listSourcesJava(), logger));
    }

    @Override
    @SuppressWarnings("unchecked")
    protected Optional<Cache.Roots> getCacheRoots(EnsoContext context) {
        return context.getPackageRepository().getPackageForLibraryJava(libraryName).map(pkg -> {
            var bindingsCacheRoot =
                    pkg.getBindingsCacheRootForPackage(Info.ensoVersion());
            var localCacheRoot = bindingsCacheRoot.resolve(libraryName.namespace());
            var distribution = context.getDistributionManager();
            var pathSegments = new String[]{
                    pkg.namespace(),
                    pkg.module(),
                    pkg.getConfig().version(),
                    Info.ensoVersion(),
                    libraryName.namespace()
            };
            var path = distribution.LocallyInstalledDirectories().irCacheDirectory()
                    .resolve(StringUtils.join(pathSegments, "/"));
            var globalCacheRoot = context.getTruffleFile(path.toFile());
            return new Cache.Roots(localCacheRoot, globalCacheRoot);
        });
    }

    @Override
    protected byte[] serialize(EnsoContext context, CachedBindings entry) throws IOException {
      var byteStream = new ByteArrayOutputStream();
      try (ObjectOutputStream stream = new ObjectOutputStream(byteStream)) {
        stream.writeObject(entry.bindings());
      }
      return byteStream.toByteArray();
    }

    static class MapToBindings implements Serializable {
        private final Map<QualifiedName, BindingsMap> _entries;

        public MapToBindings(Map<QualifiedName, BindingsMap> entries) {
            this._entries = entries;
        }

        Map<QualifiedName, BindingsMap> entries() {
            return _entries;
        }
    }

    // CachedBindings is not a record **on purpose**. There appears to be a Frgaal bug leading to invalid compilation error.
    public static final class CachedBindings {
        private final LibraryName _libraryName;
        private final MapToBindings _bindings;
        private final Optional<List<SourceFile<TruffleFile>>> _sources;

        CachedBindings(LibraryName libraryName, MapToBindings bindings, Optional<List<SourceFile<TruffleFile>>> sources) {
            this._libraryName = libraryName;
            this._bindings = bindings;
            this._sources = sources;
        }

        LibraryName libraryName() {
            return _libraryName;
        }
        MapToBindings bindings() {
            return _bindings;
        }
        Optional<List<SourceFile<TruffleFile>>> sources() {
            return _sources;
        }

    }

    public record Metadata(
            @JsonProperty("source_hash") String sourceHash,
            @JsonProperty("blob_hash") String blobHash) implements Cache.Metadata {}

    private static final String bindingsCacheDataExtension = ".bindings";

    private static final String bindingsCacheMetadataExtension =".bindings.meta";

    private final static ObjectMapper objectMapper = new ObjectMapper();

}
