package org.ethereum.net.swarm;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.spongycastle.util.encoders.Hex;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Hierarchical structure of path items
 * The item can be one of two kinds:
 * - the leaf item which contains reference to the actual data with its content type
 * - the non-leaf item which contains reference to the child manifest with the dedicated content type
 */
public class Manifest {
    public enum Status {
        OK(200),
        NOT_FOUND(404);

        private int code;
        Status(int code) {
            this.code = code;
        }
    }

    // used for Json [de]serialization only
    private static class ManifestRoot {
        public List<ManifestEntry> entries = new ArrayList<>();

        public ManifestRoot() {
        }

        public ManifestRoot(List<ManifestEntry> entries) {
            this.entries = entries;
        }

        public ManifestRoot(ManifestEntry parent) {
            entries.addAll(parent.getChildren());
        }
    }

    /**
     *  Manifest item
     */
    @JsonAutoDetect(getterVisibility= JsonAutoDetect.Visibility.NONE,
                    fieldVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
                    isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    public static class ManifestEntry extends StringTrie.TrieNode<ManifestEntry> {
        public String hash;
        public String contentType;
        public Status status;

        private Manifest thisMF;

        public ManifestEntry() {
            super(null, "");
        }

        public ManifestEntry(String path, String hash, String contentType, Status status) {
            super(null, "");
            this.path = path;
            this.hash = hash;
            this.contentType = contentType;
            this.status = status;
        }

        ManifestEntry(ManifestEntry parent, String path) {
            super(parent, path);
            this.path = path;
        }

        /**
         *  Indicates if this entry contains reference to a child manifest
         */
        public boolean isManifestType() { return MANIFEST_MIME_TYPE.equals(contentType);}
        boolean isValid() {return hash != null;}
        void invalidate() {hash = null;}

        @Override
        public boolean isLeaf() {
            return !(isManifestType() || !children.isEmpty());
        }

        /**
         *  loads the child manifest
         */
        @Override
        protected Map<String, ManifestEntry> loadChildren() {
            if (isManifestType() && children.isEmpty() && isValid()) {
                ManifestRoot manifestRoot = load(thisMF.dpa, hash);
                children = new HashMap<>();
                for (Manifest.ManifestEntry entry : manifestRoot.entries) {
                    children.put(getKey(entry.path), entry);
                }
            }
            return children;
        }

        @JsonProperty
        public String getPath() {
            return path;
        }

        @JsonProperty
        public void setPath(String path) {
            this.path = path;
        }

        @Override
        protected ManifestEntry createNode(ManifestEntry parent, String path) {
            return new ManifestEntry(parent, path).setThisMF(parent.thisMF);
        }

        @Override
        protected void nodeChanged() {
            if (!isLeaf()) {
                contentType = MANIFEST_MIME_TYPE;
                invalidate();
            }
        }

        ManifestEntry setThisMF(Manifest thisMF) {
            this.thisMF = thisMF;
            return this;
        }

        @Override
        public String toString() {
            return "ManifestEntry{" +
                    "path='" + path + '\'' +
                    ", hash='" + hash + '\'' +
                    ", contentType='" + contentType + '\'' +
                    ", status=" + status +
                    '}';
        }
    }

    public final static String MANIFEST_MIME_TYPE = "application/bzz-manifest+json";

    private DPA dpa;
    private final StringTrie<ManifestEntry> trie;

    /**
     * Constructs the Manifest instance with backing DPA storage
     * @param dpa DPA
     */
    public Manifest(DPA dpa) {
        this(dpa, new ManifestEntry(null, ""));
    }

    private Manifest(DPA dpa, ManifestEntry root) {
        this.dpa = dpa;
        trie = new StringTrie<ManifestEntry>(root.setThisMF(this)) {};
    }

    /**
     * Retrieves the entry with the specified path loading necessary nested manifests on demand
     */
    public ManifestEntry get(String path) {
        return trie.get(path);
    }

    /**
     * Adds a new entry to the manifest hierarchy with loading necessary nested manifests on demand.
     * The entry path should contain the absolute path relative to this manifest root
     */
    public void add(ManifestEntry entry) {
        add(null, entry);
    }

    void add(ManifestEntry parent, ManifestEntry entry) {
        ManifestEntry added = parent == null ? trie.add(entry.path) : trie.add(parent, entry.path);
        added.hash = entry.hash;
        added.contentType = entry.contentType;
        added.status = entry.status;
    }

    /**
     * Deletes the leaf manifest entry with the specified path
     */
    public void delete(String path) {
        trie.delete(path);
    }

    /**
     * Loads the manifest with the specified hashKey from the DPA storage
     */
    public static Manifest loadManifest(DPA dpa, String hashKey) {
        ManifestRoot manifestRoot = load(dpa, hashKey);

        Manifest ret = new Manifest(dpa);
        for (Manifest.ManifestEntry entry : manifestRoot.entries) {
            ret.add(entry);
        }
        return ret;
    }

    private static Manifest.ManifestRoot load(DPA dpa, String hashKey) {
        try {
            SectionReader sr = dpa.retrieve(new Key(hashKey));
            ObjectMapper om = new ObjectMapper();
            String s = Util.readerToString(sr);
            ManifestRoot manifestRoot = om.readValue(s, ManifestRoot.class);
            return manifestRoot;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Saves this manifest (all its modified nodes) to this manifest DPA storage
     * @return hashKey of the saved Manifest
     */
    public String save() {
        return save(trie.rootNode);
    }

    private String save(ManifestEntry e) {
        if (e.isValid()) return e.hash;
        for (ManifestEntry c : e.getChildren()) {
            save(c);
        }
        e.hash = serialize(dpa, e);
        return e.hash;
    }


    private String serialize(DPA dpa, ManifestEntry manifest) {
        try {
            ObjectMapper om = new ObjectMapper();

            ManifestRoot mr = new ManifestRoot(manifest);
            String s = om.writeValueAsString(mr);

            String hash = dpa.store(Util.stringToReader(s)).getHexString();
            return hash;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @return  manifest dump for debug purposes
     */
    public String dump() {
        return Util.dumpTree(trie.rootNode);
    }
}
