package com.vitco.app.importer;

import com.vitco.app.core.data.Data;
import com.vitco.app.util.file.FileIn;
import com.vitco.app.util.file.RandomAccessFileIn;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;

/**
 * Importer contract for (all?) voxel formats
 */
public abstract class AbstractImporter {

    // true if loading was successful
    private final boolean hasLoaded;
    public final boolean hasLoaded() {
        return hasLoaded;
    }

    // -------------

    // contains a layer object
    public final static class Layer {
        public final String name;
        // contains the voxel that were read
        private ArrayList<int[]> voxel = new ArrayList<int[]>();
        // constructor
        private Layer(String name) {
            this.name = name;
        }

        // for sanity testing
        private final HashSet<String> known = new HashSet<String>();

        private void addVoxel(int x, int y, int z, int color) {
            voxel.add(new int[]{x,y,z,color});
            // for sanity testing
            assert known.add(x+"_"+y+"_"+z);
        }

        // ----

        public final boolean isEmpty() {
            return voxel.isEmpty();
        }

        int i = 0;
        public final int[] next() {
            return voxel.get(i++);
        }

        public final boolean hasNext() {
            return i < voxel.size();
        }

        public final void reset() {
            i = 0;
        }

        private boolean visible = true;
        public void setVisible(boolean flag) {
            this.visible = flag;
        }
        public boolean isVisible() {
            return visible;
        }
    }

    // the currently active layer
    private Layer activeLayer;

    // list of layers
    public final ArrayList<Layer> getVoxel() {
        return layerList;
    }

    // add voxel to known-voxel list
    protected final void addVoxel(int x, int y, int z, int color) {
        voxelCount++;
        lx = Math.min(lx, x);
        ly = Math.min(ly, y);
        lz = Math.min(lz, z);
        hx = Math.max(hx, x);
        hy = Math.max(hy, y);
        hz = Math.max(hz, z);
        cx += x;
        cy += y;
        cz += z;
        activeLayer.addVoxel(x, y, z, color);
    }

    // contains the layer list
    private final ArrayList<Layer> layerList = new ArrayList<Layer>();
    // add a new layer that the voxels are now added to
    protected final void addLayer(String name) {
        if (activeLayer.isEmpty()) {
            layerList.remove(activeLayer);
        }
        activeLayer = new Layer(name);
        layerList.add(activeLayer);
    }
    // prepend a new layer that the voxels are now added to
    protected final void prependLayer(String name) {
        if (activeLayer.isEmpty()) {
            layerList.remove(activeLayer);
        }
        activeLayer = new Layer(name);
        layerList.add(0, activeLayer);
    }

    // set visibility of current layer
    protected final void setLayerVisibility(boolean flag) {
        activeLayer.setVisible(flag);
    }


    // --------------

    // lowest voxel value in that dimension (all layers)
    private int lx = Integer.MAX_VALUE;
    private int ly = Integer.MAX_VALUE;
    private int lz = Integer.MAX_VALUE;
    public final int[] getLowest() {
        return new int[] {lx, ly, lz};
    }

    // highest voxel value in that dimension (all layers)
    private int hx = Integer.MIN_VALUE;
    private int hy = Integer.MIN_VALUE;
    private int hz = Integer.MIN_VALUE;
    public final int[] getHighest() {
        return new int[] {hx, hy, hz};
    }

    // the amount of voxel that were added (all layers)
    private int voxelCount = 0;
    public final int getVoxelCount() {
        return voxelCount;
    }

    // weighted center (all layers)
    private long cx = 0;
    private long cy = 0;
    private long cz = 0;
    public final int[] getWeightedCenter() {
        return new int[] {(int) (cx/voxelCount), (int) (cy/voxelCount), (int) (cz/voxelCount)};
    }

    // the the center (determined by the boundaries)
    public final int[] getCenter() {
        return new int[] {Math.round(lx + (hx-lx)/2.0f), Math.round(ly + (hy-ly)/2.0f), Math.round(lz + (hz-lz)/2.0f)};
    }

    // --------------

    // constructor
    public AbstractImporter(File file, String layerName) throws IOException {
        activeLayer = new Layer(layerName);
        layerList.add(activeLayer);
        FileIn fileIn = new FileIn(file);
        try (RandomAccessFileIn raf = new RandomAccessFileIn(file, "r")) {
            hasLoaded = read(fileIn, raf);
        } finally {
            fileIn.finish();
        }
    }

    // load the data in this importer into "data"
    public void loadInto(Data data, boolean shiftToCenter) {
        if (this.hasLoaded()) {
            if (shiftToCenter) {
                int[] center = this.getWeightedCenter();
                int[] highest = this.getHighest();
                for (AbstractImporter.Layer layer : this.getVoxel()) {
                    int layerId = data.createLayer(layer.name);
                    data.selectLayer(layerId);
                    data.setVisible(layerId, layer.isVisible());
                    for (int[] vox; layer.hasNext();) {
                        vox = layer.next();
                        data.addVoxelDirect(new Color(vox[3]),
                                new int[] {vox[0] - center[0], vox[1] - highest[1], vox[2] - center[2]});
                    }
                }
            } else {
                for (AbstractImporter.Layer layer : this.getVoxel()) {
                    int layerId = data.createLayer(layer.name);
                    data.selectLayer(layerId);
                    data.setVisible(layerId, layer.isVisible());
                    for (int[] vox; layer.hasNext();) {
                        vox = layer.next();
                        data.addVoxelDirect(new Color(vox[3]),new int[] {vox[0], vox[1], vox[2]});
                    }
                }
            }
        }
    }

    // read file - returns true if file has loaded correctly
    protected abstract boolean read(FileIn fileIn, RandomAccessFileIn raf) throws IOException;
}
