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.util.ArrayDeque;
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;
import info.knacki.pass.io.CharsetHelper;
import info.knacki.pass.io.FileUtils;

public abstract class GitObject implements Comparable<GitObject>, GitPackable {
    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 {
            return FileUtils.ReadAllStream(new FileInputStream(f));
        }

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

    public static class GitRawData extends GitBlob {
        private final byte[] fData;

        GitRawData(byte[] sha1, byte[] data) {
            super(null, new byte[] { 49, 48, 48, 54, 52, 52 }, "", sha1);
            fData = data;
        }

        public GitRawData(GitTree parent, byte[] mode, String name, byte[] sha1) {
            super(parent, mode, name, sha1);
            fData = null;
        }

        public GitRawData(GitTree parent, GitBlob emptyBlob, GitRawData prev) {
            super(parent, new byte[] { 49, 48, 48, 54, 52, 52 }, emptyBlob.GetFilename(), prev.GetHash());
            fData = prev.fData;
        }

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

        @Override
        public byte[] GetPack() {
            return GetData();
        }

        public byte[] GetData() { return fData; }
    }

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

        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);
            }
            for (String i: copy.fItemOrder)
                fItemOrder.push(i);
        }

        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(GitTree parent, GitTree content, String filename) {
            this(parent, filename);
            fItems = content.fItems;
            fItemOrder = content.fItemOrder;
            for (GitObject i: fItems.values()) {
                i.SetParent(this);
            }
        }

        public static GitTree CreateEmpty() {
            GitTree tree = new GitTree(null);
            return tree.Initialize();
        }

        public GitTree Initialize() {
            fItems = new HashMap<>();
            fItemOrder = new ArrayDeque<>();
            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);
            fItemOrder.push(item.fName);
            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() {
            ArrayDeque<GitObject> objects = new ArrayDeque<>();
            for (String i: fItemOrder)
                objects.push(fItems.get(i));
            return objects;
        }

        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() +"/";
        }

        public void SetRealObject(String key, GitObject obj) {
            if (fItems.containsKey(key)) {
                fItems.put(key, obj);
            }
        }

        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);
                }
                fItemOrder.push(filename);
            } else if (nextSlash == -1) {
                Remove(filename);
                newItem = new GitBlob(this, filename, f);
                fItems.put(filename, newItem);
                fItemOrder.push(filename);
            } else {
                newItem = ((GitTree) obj).AddItem(path.substring(nextSlash + 1), f);
            }
            fSha1 = null;
            return newItem;
        }

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

        public void Fill(byte[] data) {
            int i = 0;

            while (i < data.length) {
                byte[] mode = null;
                int len;
                for (len =0; i +len <data.length && len < 8; ++len) {
                    if (data[i + len] == ' ') {
                        mode = new byte[len];
                        System.arraycopy(data, i, mode, 0, len);
                        i += len + 1;
                        break;
                    }
                }
                if (mode == null)
                    break;
                len =0;
                while (i +len < data.length && data[i +len] != 0)
                    ++len;
                byte[] filenameBytes = new byte[len];
                System.arraycopy(data, i, filenameBytes, 0, len);
                i += len +1;
                if (i +20 > data.length)
                    break;
                byte []sha1 = new byte[20];
                System.arraycopy(data, i, sha1, 0, 20);
                i += 20;
                AddItem(GitObject.factory(this, mode, CharsetHelper.ByteArrayToString(filenameBytes), sha1));
            }
        }

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

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

                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());
                }
                return str.toByteArray();
            }
            catch (IOException e) {
                return null;
            }
        }

        @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 GitRawData(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;
    }

    private void SetParent(GitTree t) { fParent = t; }

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

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