Bif.java

// <editor-fold defaultstate="collapsed" desc="license">
/*
 * Copyright (c) 2009, Karl H. Beckers
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * * Neither the name of the <ORGANIZATION> nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 **/
// </editor-fold>
package net.jarre_de_the.griffin.file;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import net.jarre_de_the.griffin.Util;
import net.jarre_de_the.griffin.types.data.DWordData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author charly4711
 */
public class Bif extends PersistableNwnFile {

    public static final String DEFAULT_FILE_TYPE = "BIFF";
    public static final String DEFAULT_FILE_VERSION = "V1  ";
    private static final Logger LOGGER = LoggerFactory.getLogger(Bif.class);

    private List<VariableBifResource> resources = new ArrayList<VariableBifResource>();

    public Bif() {
        setFileType(DEFAULT_FILE_TYPE);
        setFileVersion(DEFAULT_FILE_VERSION);
    }

    public Bif(File in) throws IOException {
        parseFile(in);
    }

    private void parseFile(File inFile)
            throws IOException {
        try (RandomAccessFile in = new RandomAccessFile(inFile, "r")) {
            // set the header fields
            for (BifHeaderField f : BifHeaderField.values()) {
                DWordData hf = DWordData.read(in);
                getHeaderFields().put(f.field(), hf);
            }

            // go to the var resource table
            in.seek(getVariableTableOffset());
            for (int i = 0; i < getVariableResourceCount(); i++) {
                VariableBifResource vbr = new VariableBifResource();
                vbr.read(in, 0);
                resources.add(vbr);
            }
        }
    }

    @Override
    public byte[] persist() throws IOException {
        ByteArrayOutputStream varResourceTable = new ByteArrayOutputStream();
        ByteArrayOutputStream varResourceData = new ByteArrayOutputStream();
        /* not used, yet
        ByteArrayOutputStream fixedResourceTable = new ByteArrayOutputStream();
        ByteArrayOutputStream fixedResourceData = new ByteArrayOutputStream();
         */

        DWordData[] header = new DWordData[BifHeaderField.values().length];

        // file type
        DWordData hf = getHeaderFields().get(BifHeaderField.FILE_TYPE.field());
        if (hf == null) {
            hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_TYPE));
        }
        header[BifHeaderField.FILE_TYPE.ordinal()] = hf;
        LOGGER.debug("ft: ");
        for (byte b : hf.getValueAsByteArray()) {
            char c = (char) b;
            LOGGER.debug("" + c);
        }
        LOGGER.debug("");

        // file version
        hf = getHeaderFields().get(BifHeaderField.FILE_VERSION.field());
        if (hf == null) {
            hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_VERSION));
        }
        header[BifHeaderField.FILE_VERSION.ordinal()] = hf;
        LOGGER.debug("fv: ");
        for (byte b : hf.getValueAsByteArray()) {
            char c = (char) b;
            LOGGER.debug("" + c);
        }
        LOGGER.debug("");

        header[BifHeaderField.VARIABLE_RES_COUNT.ordinal()]
                = new DWordData(getResources().size());
        header[BifHeaderField.FIXED_RES_COUNT.ordinal()]
                = new DWordData(getFixedResourceCount());
        header[BifHeaderField.VARIABLE_TABLE_OFFSET.ordinal()]
                = new DWordData(BifHeaderField.values().length * DWordData.LENGTH);

        // variable resource table
        // where will the variable resource data begin?
        // headers + variable table data ( 4 * 4 bytes * number of var resources ) +
        //     fixed table data (always 0 at this point)
        long offsetToVarData = (header.length * DWordData.LENGTH)
                + (4 * DWordData.LENGTH * getVariableResourceCount())
                + (5 * DWordData.LENGTH * getFixedResourceCount());
        for (int i = 0; i < this.resources.size(); i++) {
            VariableBifResource resource = getResources().get(i);

            // as per specifications there is a bitwise shifting of a value
            // x which is 0 in the patches and irrelevant, anyway, so the remaining
            // part is just y which is the index into the resource table,
            // which here is: i
            varResourceTable.write(new DWordData(i).getValueAsByteArray());
            varResourceTable.write(new DWordData(offsetToVarData).getValueAsByteArray());

            Object resourceData = resource.getData();
            byte[] resourceBytes = null;
            Class clazz = resource.getResourceType().contentType().typeClass();
            if (null != clazz) {
                if (resourceData instanceof PersistableNwnFile
                        && (Gff.class.isAssignableFrom(clazz) || TwoDa.class.isAssignableFrom(clazz))) {
                    PersistableNwnFile file = (PersistableNwnFile) resourceData;
                    resourceBytes = file.persist();
                } else if (String.class.isAssignableFrom(clazz)) {
                    resourceBytes = ((String) resourceData).getBytes(Util.CHARSET_US_ASCII);
                } else {
                    // this case does not exist, atm
                    resourceBytes = (byte[]) resourceData;
                }

            } else {
                // treat as binary
                resourceBytes = (byte[]) resourceData;
            }
            if (null != resourceBytes) {
                varResourceData.write(resourceBytes);
                offsetToVarData += resourceBytes.length;
            }

            varResourceTable.write(
                    new DWordData((resourceBytes != null
                            ? resourceBytes.length : 0)).getValueAsByteArray());
            varResourceTable.write(
                    new DWordData(resource.getResourceType().id()).getValueAsByteArray());

        }

        // put it all together
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        for (DWordData d : header) {
            out.write(d.getValueAsByteArray());
        }
        out.write(varResourceTable.toByteArray());
//        out.write(fixedResourceTable.toByteArray());
        out.write(varResourceData.toByteArray());

        return out.toByteArray();
    }

    @Override
    public Bif clone() throws CloneNotSupportedException {
        Bif clone = (Bif) super.clone();
        clone.setResources(new ArrayList<VariableBifResource>());

        // header fields not in PersistableNwnFile
        clone.setFixedResourceCount(getFixedResourceCount());
        clone.setVariableResourceCount(getVariableResourceCount());
        clone.setVariableTableOffset(getVariableTableOffset());

        for (Bif.VariableBifResource r : this.getResources()) {
            clone.getResources().add(r.clone());
        }

        return clone;
    }

    @Override
    public boolean equals(Object compare) {
        if (compare == this) {
            return true;
        }

        if (!super.equals(compare)) {
            return false;
        }

        if (!(compare instanceof Bif)) {
            return false;
        }

        Bif file = (Bif) compare;

        return areNullablePropertiesEqual(this.getResources(), file.getResources())
                && this.getFixedResourceCount() == file.getFixedResourceCount()
                && this.getVariableResourceCount() == file.getVariableResourceCount()
                && this.getVariableTableOffset() == file.getVariableTableOffset();

    }

    @Override
    public int hashCode() {
        int hash = super.hashCode();
        hash = 41 * hash + Objects.hashCode(this.resources);
        return hash;
    }

    protected enum BifHeaderField implements HeaderField {

        FILE_TYPE(PersistableNwnFile.CommonHeaderField.FILE_TYPE.field()),
        FILE_VERSION(PersistableNwnFile.CommonHeaderField.FILE_VERSION.field()),
        VARIABLE_RES_COUNT("varCount"),
        FIXED_RES_COUNT("fixCount"),
        VARIABLE_TABLE_OFFSET("varTableOff");

        private final String field;

        private BifHeaderField(String field) {
            this.field = field;
        }

        public String field() {
            return field;
        }
    }

    public long getVariableResourceCount() {
        return getHeaderNumber(BifHeaderField.VARIABLE_RES_COUNT.field(), getResources());
    }

    private void setVariableResourceCount(long bifCount) {
        getHeaderFields().put(BifHeaderField.VARIABLE_RES_COUNT.field(),
                              new DWordData(bifCount));
    }

    public long getFixedResourceCount() {
        return getHeaderNumber(BifHeaderField.FIXED_RES_COUNT.field(), null);
    }

    private void setFixedResourceCount(long bifCount) {
        getHeaderFields().put(BifHeaderField.FIXED_RES_COUNT.field(),
                              new DWordData(bifCount));
    }

    public long getVariableTableOffset() {
        return getHeaderNumber(BifHeaderField.VARIABLE_TABLE_OFFSET.field(), null);
    }

    private void setVariableTableOffset(long bifCount) {
        getHeaderFields().put(BifHeaderField.VARIABLE_TABLE_OFFSET.field(),
                              new DWordData(bifCount));
    }

    public List<VariableBifResource> getResources() {
        return resources;
    }

    public void setResources(List<VariableBifResource> resources) {
        this.resources = resources;
    }

    public static class VariableBifResource extends EmbeddedResource {

        private int id = 0;

        public static final int LENGTH = DWordData.LENGTH * 4;

        public VariableBifResource() {
        }

        @Override
        public void read(RandomAccessFile in, long offset) throws IOException {
            DWordData idData = DWordData.read(in);
            // as per docs, the game and toolset ignore the parts that are shifted
            // out by 20 bits ... sooooo
            int mask = Integer.parseInt("11111111111111111111", 2);
            id = idData.getValue() & mask;

            setOffset(DWordData.read(in).getValueAsNumber());
            setResourceLength(DWordData.read(in).getValueAsNumber());
            setResourceType(ResourceType.getResourceTypeById(
                    DWordData.read(in).getValueAsNumber().intValue()));

            long sPos = in.getFilePointer();
            // readResource jumps to the right place, need to jump back
            setData(readResource(in));
            in.seek(sPos);
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        @Override
        public VariableBifResource clone() throws CloneNotSupportedException {
            VariableBifResource clone = (VariableBifResource) super.clone();

            clone.setId(getId());

            return clone;
        }

        @Override
        public boolean equals(Object compare) {
            if (compare == this) {
                return true;
            }

            if (!super.equals(compare)) {
                return false;
            }

            if (!(compare instanceof VariableBifResource)) {
                return false;
            }

            VariableBifResource resource = (VariableBifResource) compare;

            return this.getId() == resource.getId();
        }

        @Override
        public int hashCode() {
            int hash = super.hashCode();
            hash = 71 * hash + this.id;
            return hash;
        }
    }
}