/*
 *  Copyright (C) 2010-2015 JPEXS, All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.jpexs.helpers;

import com.jpexs.decompiler.flash.helpers.Freed;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author JPEXS
 * @param <K>
 * @param <V>
 */
public class FileHashMap<K, V> extends AbstractMap<K, V> implements Freed {

    private static final Logger logger = Logger.getLogger(FileHashMap.class.getName());

    private final Map<K, Integer> lengths = new HashMap<>();

    private final Map<K, Long> offsets = new HashMap<>();

    private long fileLen = 0;

    private final RandomAccessFile file;

    private final File fileName;

    private final Set<Gap> gaps = new TreeSet<>();

    private int maxGapLen = 0;

    private boolean deleted = false;

    private static class Gap implements Comparable<Gap> {

        public long offset;

        public int length;

        public Gap(long offset, int length) {
            this.offset = offset;
            this.length = length;
        }

        @Override
        public int compareTo(Gap o) {
            return o.length - length;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 97 * hash + (int) (this.offset ^ (this.offset >>> 32));
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final Gap other = (Gap) obj;
            if (this.offset != other.offset) {
                return false;
            }
            return true;
        }
    }

    public static class FileEntry<K, V> implements Map.Entry<K, V> {

        private final FileHashMap<K, V> parent;

        private final K key;

        public FileEntry(FileHashMap<K, V> parent, K key) {
            this.parent = parent;
            this.key = key;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return parent.get(key);
        }

        @Override
        public V setValue(V value) {
            return parent.put(key, value);
        }
    }

    public FileHashMap(File file) throws IOException {
        this.file = new RandomAccessFile(file, "rw");
        this.file.setLength(0);
        this.fileName = file;
        file.deleteOnExit();
    }

    @Override
    public boolean containsKey(Object key) {
        if (deleted) {
            throw new NullPointerException();
        }
        return offsets.containsKey(key);
    }

    @Override
    public Set<K> keySet() {
        if (deleted) {
            throw new NullPointerException();
        }
        return offsets.keySet();
    }

    @Override
    public V get(Object key) {
        if (deleted) {
            throw new NullPointerException();
        }
        try {
            if (!offsets.containsKey(key)) {
                return null;
            }
            long ofs = offsets.get(key);
            int len = lengths.get(key);
            file.seek(ofs);
            byte[] data = new byte[len];
            file.readFully(data);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
            try {
                @SuppressWarnings("unchecked")
                V ret = (V) ois.readObject();
                return ret;
            } catch (ClassNotFoundException ex) {
                logger.log(Level.SEVERE, null, ex);
                return null;
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
            return null;
        }
    }

    @Override
    public synchronized V put(K key, V value) {
        if (deleted) {
            throw new NullPointerException();
        }
        ObjectOutputStream oos = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(value);
            oos.flush();
            byte[] data = baos.toByteArray();
            if (offsets.containsKey(key)) {
                long origOffset = offsets.get(key);
                int origLen = lengths.get(key);
                if (data.length <= origLen) {
                    file.seek(origOffset);
                    file.write(data);
                    lengths.put(key, data.length);
                    if (data.length < origLen) {
                        Gap g = new Gap(origOffset + data.length, origLen - data.length);
                        if (g.length > maxGapLen) {
                            maxGapLen = g.length;
                        }
                        gaps.add(g);
                    }

                    return value;
                }
            }
            if (data.length <= maxGapLen) {

                for (Iterator<Gap> i = gaps.iterator(); i.hasNext();) {
                    Gap g = i.next();
                    if (g.length >= data.length) {
                        file.seek(g.offset);
                        file.write(data);
                        offsets.put(key, g.offset);
                        lengths.put(key, g.length);
                        if (g.length > data.length) {
                            g.offset = g.offset + data.length;
                            g.length = g.length - data.length;
                        } else {
                            i.remove();
                        }
                    }
                }
            } else {
                file.seek(fileLen);
                file.write(data);
                offsets.put(key, fileLen);
                lengths.put(key, data.length);
                fileLen += data.length;
            }
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        } finally {
            try {
                oos.close();
            } catch (IOException ex) {
                // ignore
            }
        }
        return value;
    }

    @Override
    public V remove(Object objKey) {
        if (deleted) {
            throw new NullPointerException();
        }
        @SuppressWarnings("unchecked")
        K key = (K) objKey;
        if (!containsKey(key)) {
            return null;
        }
        V val = get((K) key);
        Gap g = new Gap(offsets.get(key), lengths.get(key));
        offsets.remove(key);
        lengths.remove(key);
        if (g.offset + g.length == fileLen) {
            fileLen -= g.length;
        } else {
            if (g.length > maxGapLen) {
                maxGapLen = g.length;
            }
            gaps.add(g);
        }
        return val;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        if (deleted) {
            throw new NullPointerException();
        }
        Set<Entry<K, V>> ret = new HashSet<>();
        for (K key : keySet()) {
            ret.add(new FileEntry<>(this, key));
        }
        return ret;
    }

    @Override
    public void clear() {
        if (deleted) {
            throw new NullPointerException();
        }
        offsets.clear();
        lengths.clear();
        fileLen = 0;
        maxGapLen = 0;
        try {
            file.setLength(0);
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
    }

    public void delete() {
        if (deleted) {
            throw new NullPointerException();
        }
        try {
            file.close();
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        fileName.delete();
        deleted = true;

    }

    @Override
    public boolean isFreeing() {
        return !deleted;
    }

    @Override
    public void free() {
        if (!deleted) {
            delete();
        }
    }

    @Override
    public boolean isEmpty() {
        return offsets.isEmpty();
    }

    @Override
    public int size() {
        return offsets.size();
    }
}
