package org.dynmap.exporter;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.dynmap.DynmapChunk;
import org.dynmap.DynmapCore;
import org.dynmap.DynmapWorld;
import org.dynmap.common.DynmapCommandSender;
import org.dynmap.hdmap.CustomBlockModel;
import org.dynmap.hdmap.HDBlockModels;
import org.dynmap.hdmap.HDBlockStateTextureMap;
import org.dynmap.hdmap.HDScaledBlockModels;
import org.dynmap.hdmap.HDShader;
import org.dynmap.hdmap.TexturePack.BlockTransparency;
import org.dynmap.renderer.CustomRenderer;
import org.dynmap.renderer.DynmapBlockState;
import org.dynmap.renderer.RenderPatch;
import org.dynmap.renderer.RenderPatchFactory.SideVisible;
import org.dynmap.utils.BlockStep;
import org.dynmap.utils.IndexedVector3D;
import org.dynmap.utils.IndexedVector3DList;
import org.dynmap.utils.MapChunkCache;
import org.dynmap.utils.MapIterator;
import org.dynmap.utils.PatchDefinition;
import org.dynmap.utils.PatchDefinitionFactory;

public class OBJExport {
    private final File destZipFile;     // Destination ZIP file
    private final HDShader shader;      // Shader to be used for textures
    private final DynmapWorld world;    // World to be rendered
    private final DynmapCore core;
    private final String basename;
    private int minX, minY, minZ;       // Minimum world coordinates to be rendered
    private int maxX, maxY, maxZ;       // Maximum world coordinates to be rendered
    private static Charset UTF8 = Charset.forName("UTF-8");
    private ZipOutputStream zos;        // Output stream ZIP for result
    private double originX, originY, originZ;   // Origin for exported model
    private double scale = 1.0;         // Scale for exported model
    private boolean centerOrigin = true;    // Center at origin
    private PatchDefinition[] defaultPathces;   // Default patches for solid block, indexed by BlockStep.ordinal()
    private HashSet<String> matIDs = new HashSet<String>();     // Set of defined material ids for RP
    
    private static class Face {
        String groupLine;
        String faceLine;
    }
    
    private HashMap<String, List<Face>> facesByTexture = new HashMap<String, List<Face>>();
    private static final int MODELSCALE = 16;
    private static final double BLKSIZE = 1.0 / (double) MODELSCALE;
    
    // Index of group settings
    public static final int GROUP_CHUNK = 0;
    public static final int GROUP_TEXTURE = 1;
    public static final int GROUP_BLOCKID = 2;
    public static final int GROUP_BLOCKIDMETA = 3;
    public static final int GROUP_COUNT = 4;
    private String[] group = new String[GROUP_COUNT];
    private boolean[] enabledGroups = new boolean[GROUP_COUNT];
    private String groupline = null;
    
    // Vertex set
    private IndexedVector3DList vertices;
    // UV set
    private IndexedVector3DList uvs;
    // Scaled models
    private HDScaledBlockModels models;
    
    public static final int ROT0 = 0;
    public static final int ROT90 = 1;
    public static final int ROT180 = 2;
    public static final int ROT270 = 3;
    public static final int HFLIP = 4;
    
    private static final double[][] pp = {
        { 0, 0, 0, 1, 0, 0, 0, 0, 1 },
        { 0, 1, 1, 1, 1, 1, 0, 1, 0 },
        { 1, 0, 0, 0, 0, 0, 1, 1, 0 },
        { 0, 0, 1, 1, 0, 1, 0, 1, 1 },
        { 0, 0, 0, 0, 0, 1, 0, 1, 0 },
        { 1, 0, 1, 1, 0, 0, 1, 1, 1 }
    };
    
    /**
     * Constructor for OBJ file export
     * @param dest - destination file (ZIP)
     * @param shader - shader to be used for coloring/texturing
     * @param world - world to be rendered
     * @param core - core object
     * @param basename - base file name
     */
    public OBJExport(File dest, HDShader shader, DynmapWorld world, DynmapCore core, String basename) {
        destZipFile = dest;
        this.shader = shader;
        this.world = world;
        this.core = core;
        this.basename = basename;
        this.defaultPathces = new PatchDefinition[6];
        PatchDefinitionFactory fact = HDBlockModels.getPatchDefinitionFactory();
        for (BlockStep s : BlockStep.values()) {
            double[] p = pp[s.getFaceEntered()];
            int ord = s.ordinal();
            defaultPathces[ord] = fact.getPatch(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], 0, 1, 0, 0, 1, 1, SideVisible.TOP, ord);
        }
        vertices = new IndexedVector3DList(new IndexedVector3DList.ListCallback() {
            @Override
            public void elementAdded(IndexedVector3DList list, IndexedVector3D newElement) {
                try {
                    /* Minecraft XYZ maps to OBJ YZX */
                    addStringToExportedFile(String.format(Locale.US, "v %.4f %.4f %.4f\n", 
                            (newElement.x - originX) * scale,
                            (newElement.y - originY) * scale,
                            (newElement.z - originZ) * scale
                            ));
                } catch (IOException iox) {
                }
            }
        });
        uvs = new IndexedVector3DList(new IndexedVector3DList.ListCallback() {
            @Override
            public void elementAdded(IndexedVector3DList list, IndexedVector3D newElement) {
                try {
                    addStringToExportedFile(String.format(Locale.US, "vt %.4f %.4f\n", newElement.x, newElement.y));
                } catch (IOException iox) {
                }
            }
        });
        // Get models
        models = HDBlockModels.getModelsForScale(MODELSCALE);
    }
    /**
     * Set render bounds
     * 
     * @param minx - minimum X coord
     * @param miny - minimum Y coord
     * @param minz - minimum Z coord
     * @param maxx - maximum X coord
     * @param maxy - maximum Y coord
     * @param maxz - maximum Z coord
     */
    public void setRenderBounds(int minx, int miny, int minz, int maxx, int maxy, int maxz) {
        if (minx < maxx) {
            minX = minx; maxX = maxx;
        }
        else {
            minX = maxx; maxX = minx;
        }
        if (miny < maxy) {
            minY = miny; maxY = maxy;
        }
        else {
            minY = maxy; maxY = miny;
        }
        if (minz < maxz) {
            minZ = minz; maxZ = maxz;
        }
        else {
            minZ = maxz; maxZ = minz;
        }
        if (minY < world.minY) minY = world.minY;
        if (maxY >= world.worldheight) maxY = world.worldheight - 1;
        if (centerOrigin) {
            originX = (maxX + minX) / 2.0;
            originY = minY;
            originZ = (maxZ + minZ) / 2.0;
        }
    }
    /**
     * Set origin for exported model
     * @param ox - origin x
     * @param oy - origin y
     * @param oz - origin z
     */
    public void setOrigin(double ox, double oy, double oz) {
        originX = ox;
        originY = oy;
        originZ = oz;
        centerOrigin = false;
    }
    /**
     * Set scale for exported model
     * @param scale = scale
     */
    public void setScale(double scale) {
        this.scale = scale;
    }
    /**
     * Process export
     * 
     * @param sender - command sender: use for feedback messages
     * @return true if successful, false if not
     */
    public boolean processExport(DynmapCommandSender sender) {
        boolean good = false;
        try {
            // Open ZIP file destination
            zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZipFile)));
            
            List<DynmapChunk> requiredChunks = new ArrayList<DynmapChunk>();
            int mincx = (minX >> 4);
            int maxcx = (maxX + 15) >> 4;
            int mincz = (minZ >> 4);
            int maxcz = (maxZ + 15) >> 4;
            boolean[] edgebits = new boolean[6];

            startExportedFile(basename + ".obj");
            // Add material library
            addStringToExportedFile("mtllib " + basename + ".mtl\n");

            // Loop through - do 8x8 chunks at a time (plus 1 border each way)
            for (int cx = mincx; cx <= maxcx; cx += 4) {
                for (int cz = mincz; cz <= maxcz; cz += 4) {
                    // Build chunk cache for block of chunks
                    requiredChunks.clear();
                    for (int i = -1; i < 5; i++) {
                        for (int j = -1; j < 5; j++) {
                            if (((cx+i) <= maxcx) && ((cz+j) <= maxcz) && ((cx+i) >= mincx) && ((cz+j) >= mincz)) {
                                requiredChunks.add(new DynmapChunk(cx + i, cz + j));
                            }
                        }
                    }
                    // Get the chunk buffer
                    MapChunkCache cache = core.getServer().createMapChunkCache(world, requiredChunks, true, false, true, false);
                    if (cache == null) {
                        throw new IOException("Error loading chunk cache");
                    }
                    MapIterator iter = cache.getIterator(minX, minY, minZ);
                    for (int x = cx * 16; (x < (cx * 16 + 64)) && (x <= maxX); x++) {
                        if (x < minX) x = minX;
                        edgebits[BlockStep.X_PLUS.ordinal()] = (x == minX);
                        edgebits[BlockStep.X_MINUS.ordinal()] = (x == maxX);
                        for (int z = cz * 16; (z < (cz * 16 + 64)) && (z <= maxZ); z++) {
                            if (z < minZ) z = minZ;
                            edgebits[BlockStep.Z_PLUS.ordinal()] = (z == minZ);
                            edgebits[BlockStep.Z_MINUS.ordinal()] = (z == maxZ);
                            iter.initialize(x, minY, z);
                            updateGroup(GROUP_CHUNK, "chunk" + (x >> 4) + "_" + (z >> 4));
                            // Do first (bottom)
                            edgebits[BlockStep.Y_MINUS.ordinal()] = true;
                            edgebits[BlockStep.Y_PLUS.ordinal()] = false;
                            DynmapBlockState blk = iter.getBlockType();
                            if (blk.isNotAir()) {  // Not air
                                handleBlock(blk, iter, edgebits);
                            }
                            // Do middle
                            edgebits[BlockStep.Y_MINUS.ordinal()] = false;
                            for (int y = minY + 1; y < maxY; y++) {
                                iter.setY(y);
                                blk = iter.getBlockType();
                                if (blk.isNotAir()) {  // Not air
                                    handleBlock(blk, iter, edgebits);
                                }
                            }
                            // Do top
                            edgebits[BlockStep.Y_PLUS.ordinal()] = true;
                            iter.setY(maxY);
                            blk = iter.getBlockType();
                            if (blk.isNotAir()) {  // Not air
                                handleBlock(blk, iter, edgebits);
                            }
                        }
                    }
                    // Output faces by texture
                    String grp = "";
                    for (String material : facesByTexture.keySet()) {
                        List<Face> faces = facesByTexture.get(material);
                        matIDs.add(material);   // Record material use
                        addStringToExportedFile(String.format("usemtl %s\n", material)); 
                        for (Face face : faces) {
                            if ((face.groupLine != null) && (!face.groupLine.equals(grp))) {
                                grp = face.groupLine;
                                addStringToExportedFile(grp);
                            }
                            addStringToExportedFile(face.faceLine);
                        }
                    }
                    // Clear face table
                    facesByTexture.clear();
                    // Clean up vertices we've moved past
                    vertices.resetSet(minX, minY, minZ, cx * 16 + 64, maxY, cz * 16 + 64);
                }
            }
            finishExportedFile();
            // If shader provided, add shader content to ZIP
            if (shader != null) {
                sender.sendMessage("Adding textures from shader " + shader.getName());
                shader.exportAsMaterialLibrary(sender, this);
                sender.sendMessage("Texture export completed");
            }
            // And close the ZIP
            zos.finish();
            zos.close();
            zos = null;
            good = true;
            sender.sendMessage("Export completed - " + destZipFile.getPath());
        } catch (IOException iox) {
            sender.sendMessage("Export failed: " + iox.getMessage());
        } finally {
            if (zos != null) {
                try { zos.close(); } catch (IOException e) {}
                zos = null;
                destZipFile.delete();
            }
        }
        return good;
    }
    /**
     * Start adding file to export
     * @param fname - path/name of file in destination zip
     * @throws IOException if error starting file
     */
    public void startExportedFile(String fname) throws IOException {
        ZipEntry ze = new ZipEntry(fname);
        zos.putNextEntry(ze);
    }
    /**
     * Add bytes to current exported file
     * @param buf - buffer with bytes
     * @param off - offset of start
     * @param len - length to be added
     * @throws IOException if error adding to file
     */
    public void addBytesToExportedFile(byte[] buf, int off, int len) throws IOException {
        zos.write(buf, off, len);
    }
    /**
     * Add string to curent exported file (UTF-8)
     * @param str - string to be written
     * @throws IOException if error adding to file
     */
    public void addStringToExportedFile(String str) throws IOException {
        byte[] b = str.getBytes(UTF8);
        zos.write(b, 0, b.length);
    }
    /**
     * Finish adding file to export
     * @throws IOException if error completing file
     */
    public void finishExportedFile() throws IOException {
        zos.closeEntry();
    }
    /**
     * Handle block at current iterator coord
     * @param id - block ID
     * @param iter - iterator
     * @param edgebits - bit N corresponds to side N being an endge (forge render)
     */
    private void handleBlock(DynmapBlockState blk, MapIterator map, boolean[] edgebits) throws IOException {
        BlockStep[] steps = BlockStep.values();
        int[] txtidx = null;
        // See if the block has a patch model
        RenderPatch[] patches = models.getPatchModel(blk);
        /* If no patches, see if custom model */
        if(patches == null) {
            CustomBlockModel cbm = models.getCustomBlockModel(blk);
            if (cbm != null) {   /* If so, get our meshes */
        		patches = cbm.getMeshForBlock(map);
            }
        }
        if (patches != null) {
            steps = new BlockStep[patches.length];
            txtidx = new int[patches.length];
            for (int i = 0; i < txtidx.length; i++) {
                txtidx[i] = ((PatchDefinition) patches[i]).getTextureIndex();
                steps[i] = ((PatchDefinition) patches[i]).step;
            }
        }
        else {  // See if volumetric
            short[] smod = models.getScaledModel(blk);
            if (smod != null) {
                patches = getScaledModelAsPatches(smod);
                steps = new BlockStep[patches.length];
                txtidx = new int[patches.length];
                for (int i = 0; i < patches.length; i++) {
                    PatchDefinition pd = (PatchDefinition) patches[i];
                    steps[i] = pd.step;
                    txtidx[i] = pd.getTextureIndex();
                }
            }
        }
        // Set block ID and ID+meta groups
        updateGroup(GROUP_BLOCKID, "blk" + blk.baseState.globalStateIndex);
        updateGroup(GROUP_BLOCKIDMETA, "blk" + blk.globalStateIndex);

        // Get materials for patches
        String[] mats = shader.getCurrentBlockMaterials(blk, map, txtidx, steps);
        
        if (patches != null) {  // Patch based model?
            for (int i = 0; i < patches.length; i++) {
                addPatch((PatchDefinition) patches[i], map.getX(), map.getY(), map.getZ(), mats[i]);
            }
        }
        else {
            boolean opaque = HDBlockStateTextureMap.getTransparency(blk) == BlockTransparency.OPAQUE;
            for (int face = 0; face < 6; face++) {
                DynmapBlockState blk2 = map.getBlockTypeAt(BlockStep.oppositeValues[face]);  // Get block in direction
                // If we're not solid, or adjacent block is not solid, draw side
                if ((!opaque) || blk2.isAir() || edgebits[face] || (HDBlockStateTextureMap.getTransparency(blk2) != BlockTransparency.OPAQUE)) {
                    addPatch(defaultPathces[face], map.getX(), map.getY(), map.getZ(), mats[face]);
                }
            }
        }
    }
    private int[] getTextureUVs(PatchDefinition pd, int rot) {
        int[] uv = new int[4];
        if (rot == ROT0) {
            uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
            uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
            uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
            uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
        }
        else if (rot == ROT90) {    // 90 degrees on texture
            uv[0] = uvs.getVectorIndex(1.0 - pd.vmin, pd.umin, 0);
            uv[1] = uvs.getVectorIndex(1.0 - pd.vmin, pd.umax, 0);
            uv[2] = uvs.getVectorIndex(1.0 - pd.vmax, pd.umax, 0);
            uv[3] = uvs.getVectorIndex(1.0 - pd.vmax, pd.umin, 0);
        }
        else if (rot == ROT180) {    // 180 degrees on texture
            uv[0] = uvs.getVectorIndex(1.0 - pd.umin, 1.0 - pd.vmin, 0);
            uv[1] = uvs.getVectorIndex(1.0 - pd.umax, 1.0 - pd.vmin, 0);
            uv[2] = uvs.getVectorIndex(1.0 - pd.umax, 1.0 - pd.vmax, 0);
            uv[3] = uvs.getVectorIndex(1.0 - pd.umin, 1.0 - pd.vmax, 0);
        }
        else if (rot == ROT270) {    // 270 degrees on texture
            uv[0] = uvs.getVectorIndex(pd.vmin, 1.0 - pd.umin, 0);
            uv[1] = uvs.getVectorIndex(pd.vmin, 1.0 - pd.umax, 0);
            uv[2] = uvs.getVectorIndex(pd.vmax, 1.0 - pd.umax, 0);
            uv[3] = uvs.getVectorIndex(pd.vmax, 1.0 - pd.umin, 0);
        }
        else if (rot == HFLIP) {
            uv[0] = uvs.getVectorIndex(1.0 - pd.umin, pd.vmin, 0);
            uv[1] = uvs.getVectorIndex(1.0 - pd.umax, pd.vmin, 0);
            uv[2] = uvs.getVectorIndex(1.0 - pd.umax, pd.vmax, 0);
            uv[3] = uvs.getVectorIndex(1.0 - pd.umin, pd.vmax, 0);
        }
        else {
            uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
            uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
            uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
            uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
        }
        return uv;
    }
    /**
     * Add patch as face to output
     */
    private void addPatch(PatchDefinition pd, double x, double y, double z, String material) throws IOException {
        // No material?  No face
        if (material == null) {
            return;
        }
        int rot = 0;
        int rotidx = material.indexOf('@'); // Check for rotation modifier
        if (rotidx >= 0) {
            rot = material.charAt(rotidx+1) - '0';  // 0-3
            material = material.substring(0, rotidx);
        }
        int[] v = new int[4];
        int[] uv = getTextureUVs(pd, rot);
        // Get offsets for U and V from origin
        double ux = pd.xu - pd.x0;
        double uy = pd.yu - pd.y0;
        double uz = pd.zu - pd.z0;
        double vx = pd.xv - pd.x0;
        double vy = pd.yv - pd.y0;
        double vz = pd.zv - pd.z0;
        // Offset to origin corner
        x = x + pd.x0;
        y = y + pd.y0;
        z = z + pd.z0;
        // Origin corner, offset by umin, vmin
        v[0] = vertices.getVectorIndex(x + ux*pd.umin + vx*pd.vmin, y + uy*pd.umin + vy*pd.vmin, z + uz*pd.umin + vz*pd.vmin);
        uv[0] = uvs.getVectorIndex(pd.umin, pd.vmin, 0);
        // Second is end of U (umax, vmin)
        v[1] = vertices.getVectorIndex(x + ux*pd.umax + vx*pd.vmin, y + uy*pd.umax + vy*pd.vmin, z + uz*pd.umax + vz*pd.vmin);
        uv[1] = uvs.getVectorIndex(pd.umax, pd.vmin, 0);
        // Third is end of U+V (umax, vmax)
        v[2] = vertices.getVectorIndex(x + ux*pd.umax + vx*pd.vmax, y + uy*pd.umax + vy*pd.vmax, z + uz*pd.umax + vz*pd.vmax);
        uv[2] = uvs.getVectorIndex(pd.umax, pd.vmax, 0);
        // Forth is end of V (umin, vmax)
        v[3] = vertices.getVectorIndex(x + ux*pd.umin + vx*pd.vmax, y + uy*pd.umin + vy*pd.vmax, z + uz*pd.umin + vz*pd.vmax);
        uv[3] = uvs.getVectorIndex(pd.umin, pd.vmax, 0);
        // Add patch to file
        addPatchToFile(v, uv, pd.sidevis, material, rot);
    }
    private void addPatchToFile(int[] v, int[] uv, SideVisible sv, String material, int rot) throws IOException {
        List<Face> faces = facesByTexture.get(material);
        if (faces == null) {
            faces = new ArrayList<Face>();
            facesByTexture.put(material, faces);
        }
        // If needed, rotate the UV sequence
        if (rot == HFLIP) { // Flip horizonntal
            int newuv[] = new int[uv.length];
            for (int i = 0; i < uv.length; i++) {
                newuv[i] = uv[i ^ 1];
            }
            uv = newuv;
        }
        else if (rot != ROT0) {
            int newuv[] = new int[uv.length];
            for (int i = 0; i < uv.length; i++) {
                newuv[i] = uv[(i+4-rot) % uv.length];
            }
            uv = newuv;
        }
        Face f = new Face();
        f.groupLine = updateGroup(GROUP_TEXTURE, material);
        switch (sv) {
            case TOP:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]); 
                break;
            case TOPFLIP:
                f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[2], v[2], uv[3], v[1], uv[0], v[0], uv[1]); 
                break;
            case TOPFLIPV:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]); 
                break;
            case TOPFLIPHV:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]); 
                break;
            case BOTTOM:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[3], v[2], uv[2], v[1], uv[1], v[0], uv[0]); 
                break;
            case BOTH:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]); 
                f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[3], v[2], uv[2], v[1], uv[1], v[0], uv[0]); 
                break;
            case FLIP:
                f.faceLine = String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[0], uv[0], v[1], uv[1], v[2], uv[2], v[3], uv[3]); 
                f.faceLine += String.format("f %d/%d %d/%d %d/%d %d/%d\n", v[3], uv[2], v[2], uv[3], v[1], uv[0], v[0], uv[1]); 
                break;
        }
        faces.add(f);
    }
    
    public Set<String> getMaterialIDs() {
        return matIDs;
    }
    
    private static final boolean getSubblock(short[] mod, int x, int y, int z) {
        if ((x >= 0) && (x < MODELSCALE) && (y >= 0) && (y < MODELSCALE) && (z >= 0) && (z < MODELSCALE)) {
            return mod[MODELSCALE*MODELSCALE*y + MODELSCALE*z + x] != 0;
        }
        return false;
    }
    // Scan along X axis
    private int scanX(short[] tmod, int x, int y, int z) {
        int xlen = 0;
        while (getSubblock(tmod, x+xlen, y, z)) { 
            xlen++;
        }
        return xlen;
    }
    // Scan along Z axis for rows matching given x length
    private int scanZ(short[] tmod, int x, int y, int z, int xlen) {
        int zlen = 0;
        while (scanX(tmod, x, y, z+zlen) >= xlen) {
            zlen++;
        }
        return zlen;
    }
    // Scan along Y axis for layers matching given X and Z lengths
    private int scanY(short[] tmod, int x, int y, int z, int xlen, int zlen) {
        int ylen = 0;
        while (scanZ(tmod, x, y+ylen, z, xlen) >= zlen) {
            ylen++;
        }
        return ylen;
    }
    private void addSubblock(short[] tmod, int x, int y, int z, List<RenderPatch> list) {
        // Find dimensions of cuboid
        int xlen = scanX(tmod, x, y, z);
        int zlen = scanZ(tmod, x, y, z, xlen);
        int ylen = scanY(tmod, x, y, z, xlen, zlen);
        // Add equivalent of boxblock
        CustomRenderer.addBox(HDBlockModels.getPatchDefinitionFactory(), list, 
                BLKSIZE * x, BLKSIZE * (x+xlen), 
                BLKSIZE * y, BLKSIZE * (y+ylen),
                BLKSIZE * z, BLKSIZE * (z+zlen), 
                HDBlockModels.boxPatchList);
        // And remove blocks from model (since we have them covered)
        for (int xx = 0; xx < xlen; xx++) {
            for (int yy = 0; yy < ylen; yy++) {
                for (int zz = 0; zz < zlen; zz++) {
                    tmod[MODELSCALE*MODELSCALE*(y+yy) + MODELSCALE*(z+zz) + (x+xx)] = 0;
                }
            }
        }
    }
    private PatchDefinition[] getScaledModelAsPatches(short[] mod) {
        ArrayList<RenderPatch> list = new ArrayList<RenderPatch>();
        short[] tmod = Arrays.copyOf(mod, mod.length);  // Make copy
        for (int y = 0; y < MODELSCALE; y++) {
            for (int z = 0; z < MODELSCALE; z++) {
                for (int x = 0; x < MODELSCALE; x++) {
                    if (getSubblock(tmod, x, y, z)) {   // If occupied, try to add to list
                        addSubblock(tmod, x, y, z, list);
                    }
                }
            }
        }
        PatchDefinition[] pd = new PatchDefinition[list.size()];
        for (int i = 0; i < pd.length; i++) {
            pd[i] = (PatchDefinition) list.get(i);
        }
        return pd;
    }
    
    private String updateGroup(int grpIndex, String newgroup) {
        if (enabledGroups[grpIndex]) {
            if (!newgroup.equals(group[grpIndex])) {
                group[grpIndex] = newgroup;
                String newline = "g";
                for (int i = 0; i < GROUP_COUNT; i++) {
                    if (enabledGroups[i]) {
                        newline += " " + group[i];
                    }
                }
                newline += "\n";
                groupline = newline;
            }
        }
        return groupline;
    }
    
    public boolean getGroupEnabled(int grpIndex) {
        if (grpIndex < enabledGroups.length) {
            return enabledGroups[grpIndex];
        }
        else {
            return false;
        }
    }
    public void setGroupEnabled(int grpIndex, boolean set) {
        if (grpIndex < enabledGroups.length) {
            enabledGroups[grpIndex] = set;
        }
    }
    public String getBaseName() {
        return basename;
    }
}
