package info.knacki.pass.git.entities;

import android.support.annotation.NonNull;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;

import info.knacki.pass.git.GitSha1;

public abstract class GitObject implements Comparable<GitObject>, GitPackable {
    final GitTree fParent;
    final byte[] fMode;
    final String fName;
    byte[] fSha1;

    @Override
    public int compareTo(@NonNull GitObject gitObject) {
        return fName.toLowerCase().compareTo(gitObject.fName.toLowerCase());
    }

    public static class GitBlob extends GitObject {
        private File fFile;

        private GitBlob(GitTree parent, byte[] mode, String name, byte[] sha1) {
            super(parent, mode, name, sha1);
            fFile = null;
        }

        private GitBlob(GitTree parent, GitBlob copy) {
            this(parent, copy.fMode, copy.fName, copy.fSha1);
        }

        GitBlob(GitTree parent, String filename, File f) {
            this(parent, new byte[] { 49, 48, 48, 54, 52, 52 }, filename, GitSha1.getRawSha1OfFile(f));
            fFile = f;
        }

        @Override
        public eType GetPackableType() {
            return eType.eType_Blob;
        }

        public static byte[] GetFilePack(File f) throws IOException {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            InputStream input = new FileInputStream(f);
            int numRead = 0;
            while (numRead != -1)
            {
                byte[] buffer = new byte[1024];
                numRead = input.read(buffer);
                if (numRead > 0)
                    out.write(buffer, 0, numRead);
            }
            input.close();
            return out.toByteArray();
        }

        @Override
        public byte[] GetPack() {
            try
            {
                return GetFilePack(fFile);
            }
            catch (FileNotFoundException e) {
                return null;
            }
            catch(IOException t) {
                t.printStackTrace();
            }
            return null;
        }
    }

    public static class GitTree extends GitObject {
        HashMap<String, GitObject> fItems = null;

        private GitTree(GitTree parent, byte[] mode, String name, byte[] sha1) {
            super(parent, mode, name, sha1);
        }

        GitTree(GitTree parent, GitTree copy) {
            this(parent, copy.fMode, copy.fName, copy.fSha1);
            Initialize();
            for (Map.Entry<String, GitObject> o: copy.fItems.entrySet()) {
                GitObject src = o.getValue();
                GitObject oCopy = src instanceof GitTree ? new GitTree(this, (GitTree) src) : new GitBlob(this, (GitBlob) src);
                fItems.put(o.getKey(), oCopy);
            }
        }

        GitTree(byte[] sha1) {
            super(null, new byte[] { '4', '0', '0', '0', '0' }, "", sha1);
        }

        private GitTree(GitTree parent, String filename) {
            super(parent, new byte[] { '4', '0', '0', '0', '0' }, filename, null);
        }

        public GitTree Initialize() {
            fItems = new HashMap<>();
            return this;
        }

        boolean IsInitialized() {
            return null != fItems;
        }

        public GitObject GetObject(String name) {
            return fItems.get(name);
        }

        public GitObject GetObjectFullPath(String path) {
            if ('/' == path.charAt(0))
                path = path.substring(1);
            final int nextSlash = path.indexOf('/');
            final String filename = nextSlash == -1 ? path : (path.substring(0, nextSlash));
            final GitObject obj = GetObject(filename);
            if (obj == null || (obj instanceof GitBlob && nextSlash != -1))
                return null;
            if (nextSlash == -1)
                return obj;
            return ((GitTree) obj).GetObjectFullPath(path.substring(nextSlash +1));
        }

        public GitTree AddItem(GitObject item) {
            fItems.put(item.fName, item);
            return this;
        }

        public GitTree FindNextNotInitializedTree() {
            if (!IsInitialized())
                return this;
            for (GitObject obj: fItems.values()) {
                if (!(obj instanceof GitTree))
                    continue;
                GitTree child = ((GitTree) obj).FindNextNotInitializedTree();
                if (child != null)
                    return child;
            }
            return null;
        }

        public Iterable<GitObject> GetObjects() {
            return new TreeSet<>(fItems.values());
        }

        public Iterable<GitBlob> GetBlobs() {
            SortedSet<GitBlob> objects = new TreeSet<>();
            for (GitObject i: fItems.values())
                if (i instanceof GitBlob)
                    objects.add((GitBlob) i);
            return objects;
        }

        public Iterable<GitTree> GetTrees() {
            SortedSet<GitTree> objects = new TreeSet<>();
            for (GitObject i: fItems.values())
                if (i instanceof GitTree)
                    objects.add((GitTree) i);
            return objects;
        }

        public String GetPath() {
            if (fParent != null)
                return fParent.GetPath() +GetFilename() +"/";
            return GetFilename() +"/";
        }

        private void FindAllBlobs(HashMap<String, GitBlob> data) {
            String rootPath = GetPath();
            for (GitBlob i: GetBlobs())
                data.put(rootPath +i.GetFilename(), i);
            for (GitTree i: GetTrees())
                i.FindAllBlobs(data);
        }

        public HashMap<String, GitBlob> FindAllBlobs() {
            HashMap<String, GitBlob> result = new HashMap<>();
            FindAllBlobs(result);
            return result;
        }

        public boolean IsRoot() {
            return fParent == null;
        }

        public GitObject AddItem(String path, File f) {
            if ('/' == path.charAt(0))
                path = path.substring(1);
            final int nextSlash = path.indexOf('/');
            final String filename = nextSlash == -1 ? path : (path.substring(0, nextSlash));
            final GitObject obj = GetObject(filename);
            final GitObject newItem;

            if (obj == null) {
                if (nextSlash != -1) {
                    GitTree child = new GitTree(this, filename).Initialize();
                    newItem = child.AddItem(path.substring(nextSlash + 1), f);
                    fItems.put(filename, child);
                } else {
                    newItem = new GitBlob(this, filename, f);
                    fItems.put(filename, newItem);
                }
            } else if (nextSlash == -1) {
                Remove(filename);
                newItem = new GitBlob(this, filename, f);
                fItems.put(filename, newItem);
            } else {
                newItem = ((GitTree) obj).AddItem(path.substring(nextSlash + 1), f);
            }
            fSha1 = null;
            return newItem;
        }

        public GitTree Remove(String filename) {
            fItems.remove(filename);
            fSha1 = null;
            return this;
        }

        @Override
        public eType GetPackableType() {
            return eType.eType_Tree;
        }

        @Override
        public byte[] GetPack() {
            ByteArrayOutputStream str = new ByteArrayOutputStream();

            try {
                for (GitObject go : GetObjects()) {
                    str.write((go.GetMode() + ' ').getBytes());
                    if (go.fName.equals("Aa") || "Oo".equals(go.fName))
                        Logger.getLogger(GitObject.class.getName()).severe("out file in tree " +go.GetGitPath() +"$" + GitSha1.BytesToString(go.GetHash()));
                    str.write(go.GetFilename().getBytes());
                    str.write(new byte[]{0});
                    str.write(go.GetHash());
                }
            }
            catch (IOException e) {}
            return str.toByteArray();
        }

        @Override
        public byte[] GetHash() {
            if (null != fSha1)
                return fSha1;
            return fSha1 = GitSha1.getRawSha1OfPackable(this);
        }
    }

    private GitObject(GitTree parent, byte[] mode, String name, byte[] sha1) {
        fParent = parent;
        fMode = mode;
        fName = name;
        fSha1 = sha1;
    }

    public static GitObject factory(GitTree parent, byte[] mode, String name, byte[] sha1) {
        if (mode[0] == '4') {
            return new GitTree(parent, mode, name, sha1);
        }
        return new GitBlob(parent, mode, name, sha1);
    }

    public byte[] GetHash() {
        return fSha1;
    }

    public String GetFilename() {
        return fName;
    }

    public String GetGitPath() {
        StringBuilder sb = new StringBuilder();
        GitTree parent = fParent;
        sb.insert(0, fName);
        while (parent != null) {
            sb.insert(0, parent.fName + "/");
            parent = parent.fParent;
        }
        return sb.toString();
    }

    public GitTree GetParent() {
        return fParent;
    }

    public String GetMode() {
        return new String(fMode);
    }

    public int GetDepth() {
        return fParent == null ? 1 : fParent.GetDepth() +1;
    }
}
