package net.minecraftforge.fml.common;

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Set;

import net.minecraftforge.fml.common.asm.transformers.ModAPITransformer;
import net.minecraftforge.fml.common.discovery.ASMDataTable;
import net.minecraftforge.fml.common.discovery.ModCandidate;
import net.minecraftforge.fml.common.discovery.ModDiscoverer;
import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData;
import net.minecraftforge.fml.common.functions.ModIdFunction;
import net.minecraftforge.fml.common.versioning.ArtifactVersion;
import net.minecraftforge.fml.common.versioning.DefaultArtifactVersion;
import net.minecraftforge.fml.common.versioning.VersionParser;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

public class ModAPIManager {
    public static final ModAPIManager INSTANCE = new ModAPIManager();
    private ModAPITransformer transformer;
    private ASMDataTable dataTable;
    private Map<String,APIContainer> apiContainers;

    private static class APIContainer extends DummyModContainer {
        private List<ArtifactVersion> referredMods;
        private ArtifactVersion ownerMod;
        private ArtifactVersion ourVersion;
        private String providedAPI;
        private File source;
        private String version;
        private Set<String> currentReferents;
        private Set<String> packages;
        private boolean selfReferenced;

        public APIContainer(String providedAPI, String apiVersion, File source, ArtifactVersion ownerMod)
        {
            this.providedAPI = providedAPI;
            this.version = apiVersion;
            this.ownerMod = ownerMod;
            this.ourVersion = new DefaultArtifactVersion(providedAPI, apiVersion);
            this.referredMods = Lists.newArrayList();
            this.source = source;
            this.currentReferents = Sets.newHashSet();
            this.packages = Sets.newHashSet();
        }

        @Override
        public File getSource()
        {
            return source;
        }
        @Override
        public String getVersion()
        {
            return version;
        }
        @Override
        public String getName()
        {
            return "API: "+providedAPI;
        }
        @Override
        public String getModId()
        {
            return providedAPI;
        }
        @Override
        public List<ArtifactVersion> getDependants()
        {
            return referredMods;
        }

        @Override
        public List<ArtifactVersion> getDependencies()
        {
            return selfReferenced ? ImmutableList.<ArtifactVersion>of() : ImmutableList.of(ownerMod);
        }

        @Override
        public ArtifactVersion getProcessedVersion()
        {
            return ourVersion;
        }

        public void validate(String providedAPI, String apiOwner, String apiVersion)
        {
            // TODO Compare this annotation data to the one we first found. Maybe barf if there is inconsistency?
        }

        @Override
        public String toString()
        {
            return "APIContainer{"+providedAPI+":"+version+"}";
        }

        public void addAPIReference(String embedded)
        {
            if (currentReferents.add(embedded))
            {
                referredMods.add(VersionParser.parseVersionReference(embedded));
            }
        }

        public void addOwnedPackage(String apiPackage)
        {
            packages.add(apiPackage);
        }

        public void addAPIReferences(List<String> candidateIds)
        {
            for (String modId : candidateIds)
            {
                addAPIReference(modId);
            }
        }

        void markSelfReferenced()
        {
            selfReferenced = true;
        }
    }
    public void registerDataTableAndParseAPI(ASMDataTable dataTable)
    {
        this.dataTable = dataTable;

        Set<ASMData> apiList = dataTable.getAll("net.minecraftforge.fml.common.API");

        apiContainers = Maps.newHashMap();

        for (ASMData data : apiList)
        {
            Map<String, Object> annotationInfo = data.getAnnotationInfo();
            String apiPackage = data.getClassName().substring(0,data.getClassName().indexOf(".package-info"));
            String providedAPI = (String) annotationInfo.get("provides");
            String apiOwner = (String) annotationInfo.get("owner");
            String apiVersion = (String) annotationInfo.get("apiVersion");
            APIContainer container = apiContainers.get(providedAPI);
            if (container == null)
            {
                container = new APIContainer(providedAPI, apiVersion, data.getCandidate().getModContainer(), VersionParser.parseVersionReference(apiOwner));
                apiContainers.put(providedAPI, container);
            }
            else
            {
                container.validate(providedAPI, apiOwner, apiVersion);
            }
            container.addOwnedPackage(apiPackage);
            for (ModContainer mc : data.getCandidate().getContainedMods())
            {
                String embeddedIn = mc.getModId();
                if (container.currentReferents.contains(embeddedIn))
                {
                    continue;
                }
                FMLLog.fine("Found API %s (owned by %s providing %s) embedded in %s",apiPackage, apiOwner, providedAPI, embeddedIn);
                if (!embeddedIn.equals(apiOwner))
                {
                    container.addAPIReference(embeddedIn);
                }
            }
        }

        for (APIContainer container : apiContainers.values())
        {
            for (String pkg : container.packages)
            {
                Set<ModCandidate> candidates = dataTable.getCandidatesFor(pkg);
                for (ModCandidate candidate : candidates)
                {
                    List<String> candidateIds = Lists.transform(candidate.getContainedMods(), new ModIdFunction());
                    if (!candidateIds.contains(container.ownerMod.getLabel()) && !container.currentReferents.containsAll(candidateIds))
                    {
                        FMLLog.info("Found mod(s) %s containing declared API package %s (owned by %s) without associated API reference",candidateIds, pkg, container.ownerMod);
                        container.addAPIReferences(candidateIds);
                    }
                }
            }
            if (apiContainers.containsKey(container.ownerMod.getLabel()))
            {
                ArtifactVersion owner = container.ownerMod;
                do
                {
                    APIContainer parent = apiContainers.get(owner.getLabel());
                    if (parent == container)
                    {
                        FMLLog.finer("APIContainer %s is it's own parent. skipping", owner);
                        container.markSelfReferenced();
                        break;
                    }
                    FMLLog.finer("Removing upstream parent %s from %s", parent.ownerMod.getLabel(), container);
                    container.currentReferents.remove(parent.ownerMod.getLabel());
                    container.referredMods.remove(parent.ownerMod);
                    owner = parent.ownerMod;
                }
                while (apiContainers.containsKey(owner.getLabel()));
            }
            FMLLog.fine("Creating API container dummy for API %s: owner: %s, dependents: %s", container.providedAPI, container.ownerMod, container.referredMods);
        }
    }

    public void manageAPI(ModClassLoader modClassLoader, ModDiscoverer discoverer)
    {
        registerDataTableAndParseAPI(discoverer.getASMTable());
        transformer = modClassLoader.addModAPITransformer(dataTable);
    }

    public void injectAPIModContainers(List<ModContainer> mods, Map<String, ModContainer> nameLookup)
    {
        mods.addAll(apiContainers.values());
        nameLookup.putAll(apiContainers);
    }

    public void cleanupAPIContainers(List<ModContainer> mods)
    {
        mods.removeAll(apiContainers.values());
    }

    public boolean hasAPI(String modId)
    {
        return apiContainers.containsKey(modId);
    }

    public Iterable<? extends ModContainer> getAPIList()
    {
        return apiContainers.values();
    }
}