StructData.java

// <editor-fold defaultstate="collapsed" desc="license">
/*
 * Copyright (c) 2014, 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.types.data;

import java.io.IOException;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.jarre_de_the.griffin.Util;
import net.jarre_de_the.griffin.exception.LabelNotUniqueException;
import net.jarre_de_the.griffin.exception.NoFieldFoundException;
import net.jarre_de_the.griffin.file.Gff;
import net.jarre_de_the.griffin.types.field.*;
import net.jarre_de_the.griffin.types.field.AbstractField.FieldType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of STRUCT. The GFF STRUCT physically contains an ID, a pointer
 * to where the contained elements are stored and the element count. The STRUCT
 * data type contains a number of fields including other STRUCTs.
 * <p>
 * @author charly4711
 */
public class StructData
        extends AbstractData
        implements Container, Cloneable {

    /**
     * The size of a STRUCT in number of bytes for the STRUCT element itself,
     * not including the fields contained (only the pointer).
     */
    public static final int STRUCT_LENGTH = 12;

    private static final Logger LOGGER = LoggerFactory.getLogger(StructData.class);

    private DWordData id;

    private List<AbstractField> fields;

    /*
     *
     * constructors
     *
     */
    public StructData() {
        id = new DWordData(-1);
    }

    /**
     * Creates a new instance of {@code StructData} and sets the specified
     * values for id and fields contained.
     * <p>
     * @param id The field ID to set.
     * @param fields The fields to be contained in the new {@code StructData}.
     */
    public StructData(DWordData id,
                      List<AbstractField> fields) {
        setId(id);
        this.fields = fields;
    }

    /*
     *
     * read from file
     *
     */
    /**
     * Creates a new instance of {@code StructData} based on the data read from
     * the file passed. Note that the contents are read from whatever position
     * the file pointer is at.
     * <p>
     * @param in An open {@code RandomAccessFile}.
     * @return
     * @throws java.io.IOException If there are any I/O related problems with
     * file access to in.
     */
    public static StructData read(RandomAccessFile in,
                                  Gff file)
            throws IOException {
        StructData ret = new StructData();

        LOGGER.debug("Reading StructData from file.");
        // read ID first
        ret.setId(DWordData.read(in));
        LOGGER.trace("Read this id: " + ret.getId().getValueAsNumber());
        // offset is next
        long offset = DWordData.read(in).getValueAsNumber();
        LOGGER.trace("Read this offset: " + offset);
        // field count next
        long fcount = DWordData.read(in).getValueAsNumber();
        LOGGER.trace("Have to read " + fcount + " fields.");

        // go and fetch the fields from the known offset and to the known count
        ret.setFieldValue(readFields(in,
                                     (int) offset, (int) fcount, file));

        LOGGER.debug("Done reading StructData.");
        return ret;
    }

    /**
     * Reads the fields contained in this {@code StructData} from a GFF file.
     * This is called by {@link #read(java.io.RandomAccessFile, net.jarre_de_the.griffin.object.File)
     * }
     * and gets the correct offset into the field or field index array and the
     * correct field count from there.
     * <p>
     * @param in File to read from.
     * @param offset Offset into the field array or field index array depending
     * on the {@code count}.
     * @param count Number of fields contained in this instance.
     * @param file File object with the required information about the various
     * GFF file arrays.
     * @return A list of fields contained in this instance.
     * @throws java.io.IOException If there are any I/O related problems with
     * file access to in.
     */
    private static List<AbstractField> readFields(RandomAccessFile in,
                                                  int offset,
                                                  int count,
                                                  Gff file)
            throws IOException {
        List<AbstractField> lFields = new ArrayList<AbstractField>();

        // return a null list, if there are no children
        if (count == 0) {
            LOGGER.trace("Was asked to read a list of fields, where "
                    + "the number of fields to read was zero.");
            return null;
        }

        // if there are more than one field in this struct ...
        if (count > 1) {
            LOGGER.trace("Was asked to read a list of fields.");
            // we need to look for pointers to the fields themselves off the
            // field index ... what we retrieved as offset should be a byte
            // offset to the field index
            long fOff = file.getFieldIdxOff() + file.getEmbeddedOffset();
            fOff += offset;
            LOGGER.trace("Absolute field index offset: " + fOff);

            // go there
            in.seek(fOff);
            for (int i = 0; i < count; i++) {
                // then start reading one DWord after another
                long fPos = DWordData.read(in).getValueAsNumber();
                fPos = (fPos * AbstractField.FIELD_LENGTH) + file.getFieldOff() + file.getEmbeddedOffset();
                LOGGER.trace("Absolute field offset: " + fPos);
                // store the position in the field index (which is that of the
                // next element to read
                long fIdxPos = in.getFilePointer();

                in.seek(fPos);
                lFields.add(AbstractField.readField(in, file));

                in.seek(fIdxPos);
            }
        } else {
            LOGGER.trace("Was asked to read a single field.");
            long fOff = file.getFieldOff() + file.getEmbeddedOffset();
            fOff += (1L * offset * AbstractField.FIELD_LENGTH);
            LOGGER.trace("Absolute field offset: " + fOff);
            in.seek(fOff);
            lFields.add(AbstractField.readField(in, file));
        }

        LOGGER.trace("Returning " + lFields.size() + " fields.");
        return lFields;
    }

    /*
     *
     * setter
     *
     */
    private void setId(DWordData id) {
        if (null != id) {
            try {
                this.id = id.clone();
            } catch (CloneNotSupportedException ex) {
                LOGGER.error(null, ex);
            }
        } else {
            this.id = null;
        }
    }

    private void setFieldValue(List<AbstractField> fields) {
        this.fields = fields;
    }

    /*
     *
     * getter
     *
     */
    public DWordData getId() {
        DWordData result = null;
        if (null != id) {
            try {
                result = id.clone();
            } catch (CloneNotSupportedException ex) {
                LOGGER.error(null, ex);
            }
        }
        return result;
    }

    /**
     * Retrieves the list of fields contained in this instance.
     * <p>
     * @return This instance's fields.
     */
    public List<AbstractField> getValueAsList() {
        return fields;
    }

    /**
     * Retrieves a specific, numbered element of the fields contained in this
     * instance.
     * <p>
     * @param i Index of the element to return from this instance's list of
     * contained fields.
     * @return A single field contained in this instance.
     */
    public AbstractField getValueElement(int i) {
        if (fields != null && fields.size() > i) {
            return fields.get(i);
        } else {
            return null;
        }
    }

    /*
     *
     * utility
     *
     */
    /**
     * Implements Container
     * <p>
     * @param clazz
     * @param label
     * @return
     */
    @Override
    public <T extends AbstractField> List<FoundField<T>>
            findField(Class<T> clazz, byte[] label) {
        return findField(clazz, label, null, null, null);
    }

    @Override
    public <T extends AbstractField, M extends AbstractData> List<FoundField<T>>
            findField(Class<T> clazz, byte[] label, M value) {
        return findField(clazz, label, value, null, null);
    }

    @Override
    public <T extends AbstractField> List<FoundField<T>>
            findField(Class<T> clazz, byte[] label, String regex) {
        return findField(clazz, label, null, regex, null);
    }

    protected <T extends AbstractField, M extends AbstractData> List<FoundField<T>>
            findField(Class<T> clazz, byte[] label,
                      M value, String regex, int[] path) {
        List<FoundField<T>> ret = new ArrayList<FoundField<T>>();
        LOGGER.debug("Trying to find field "
                + Util.getNullTerminatedString(label)
                + " of type: " + (clazz == null ? "null" : clazz.getSimpleName()));

        if (this.fields != null) {
            if (this.fields.size() > 0) {
                LOGGER.trace("Need to examine " + this.fields.size()
                        + " fields.");
                for (int i = 0; i < fields.size(); i++) {
                    T ef = (T) fields.get(i);

                    int[] localPath;
                    if (null != path) {
                        localPath = new int[1 + path.length];
                        System.arraycopy(path, 0, localPath, 0, path.length);
                        localPath[localPath.length - 1] = i;
                    } else {
                        localPath = new int[]{i};
                    }
                    examineField(ret, ef, clazz, label, value, regex, localPath);
                }
            } else {
                LOGGER.trace("No fields to examine.");
            }
        } else {
            LOGGER.trace("No fields to examine.");
        }
        LOGGER.debug(ret.size() + " elements found.");
        return ret;
    }

    private <T extends AbstractField, M extends AbstractData> void examineField(
            List<FoundField<T>> found,
            T examine,
            Class<T> clazz,
            byte[] label,
            M value,
            String regex,
            int[] path) {
        List<FoundField<T>> f;

        if (null != examine) {
            if (clazz == null
                    || examine.getType().getFieldClassOfType().equals(clazz)) {
                if (label == null || Arrays.equals(examine.getLabel(),
                                                   label)) {
                    boolean match = false;
                    if (null != value) {
                        if (value.equals(examine.getData())) {
                            match = true;
                        }
                    } else if (null != regex) {
                        if (null != examine.getData()
                                && examine.getData().toString().matches(regex)) {
                            match = true;
                        }
                    } else {
                        match = true;
                    }

                    if (match) {
                        LOGGER.trace("Match found.");
                        found.add(new FoundField<T>(examine, path));
                    }
                }
            }

            if (examine.getType().equals(AbstractField.FieldType.Struct)) {
                LOGGER.trace("StructField found, recursing.");
                StructField s = (StructField) examine;
                StructData sd = s.getValue();
                if (sd != null) {
                    f = sd.findField(clazz, label, value, regex, path);
                    if (f != null) {
                        found.addAll(f);
                    }
                }
            } else if (examine.getValue() != null && examine.getValue() instanceof Container) {
                LOGGER.trace(examine.getType() + " found, recursing.");
                Container c = (Container) examine.getValue();
                if (null != value) {
                    f = c.findField(clazz, label, value);
                } else if (null != regex) {
                    f = c.findField(clazz, label, regex);
                } else {
                    f = c.findField(clazz, label);
                }

                if (f != null) {
                    for (FoundField<T> foundField : f) {
                        int[] localPath;
                        if (null != path) {
                            localPath = new int[foundField.getPath().length + path.length];
                            System.arraycopy(path, 0, localPath, 0, path.length);
                            System.arraycopy(foundField.getPath(), 0, localPath, path.length, foundField.getPath().length);
                        } else {
                            // this cannot really happen
                            // path can only be null if findFields is called on
                            // the struct itself and then we cannot be in a ListField
                            localPath = foundField.getPath();
                        }

                        found.add(new FoundField<T>(foundField.getField(), localPath));
                    }
                }
            }
        }
    }

    /**
     * Implements Container
     * <p>
     * @param clazz
     * @param label
     * @return
     */
    @Override
    public <T extends AbstractField> T findLocalField(Class<T> clazz, byte[] label) {
        LOGGER.debug("Trying to find local field "
                + Util.getNullTerminatedString(label)
                + " of type: " + (clazz == null ? "null" : clazz.getSimpleName()));
        T result = null;

        if (null != fields && null != label) {
            if (fields.size() > 0) {
                LOGGER.trace("Need to examine " + fields.size()
                        + " fields.");
                for (AbstractField field : fields) {
                    T f = (T) field;
                    if ((null == clazz || f.getType().getFieldClassOfType().equals(clazz))
                            && Arrays.equals(f.getLabel(), label)) {
                        LOGGER.trace("Match found.");
                        if (result != null) {
                            FieldType ft = null;
                            for (FieldType type : FieldType.values()) {
                                if (type.getFieldClassOfType().equals(clazz)) {
                                    ft = type;
                                    break;
                                }
                            }
                            throw new LabelNotUniqueException(this, ft, label);
                        }
                        result = f;
                    }
                }
            } else {
                LOGGER.trace("No fields to examine.");
            }
        } else {
            LOGGER.trace("No fields to examine or no label specified.");
        }

        if (result == null) {
            FieldType ft = null;
            for (FieldType f : FieldType.values()) {
                if (f.getFieldClassOfType().equals(clazz)) {
                    ft = f;
                    break;
                }
            }
            throw new NoFieldFoundException(this, ft, label);
        } else {
            return result;
        }
    }

    @Override
    public AbstractField findFieldByPath(int[] path) {
        if (path.length > 0) {
            int here = path[0];
            AbstractField tmp = this.getValueElement(here);
            if (path.length == 1) {
                return tmp;
            } else if (tmp != null && tmp.getValue() != null
                    && tmp.getValue() instanceof Container) {
                return ((Container) tmp.getValue()).findFieldByPath(Arrays.copyOfRange(path, 1, path.length));
            }
        }
        throw new NoFieldFoundException(this, path);
    }

    @Override
    public StructData clone() throws CloneNotSupportedException {
        LOGGER.debug("Cloning object");
        StructData clone = (StructData) super.clone();

        clone.setId(id);
        List<AbstractField> fl = new ArrayList<AbstractField>();
        if (fields != null) {
            for (AbstractField f : fields) {
                fl.add(f.clone());
            }
            clone.setFieldValue(fl);
        }
        return clone;
    }

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

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

        StructData fl = (StructData) compare;

        if (fl.getValueAsList() == null && fields == null) {
            return true && id.equals(fl.getId());
        } else if ((fl.getValueAsList() == null && fields != null) || (fl.getValueAsList() != null && fields == null)) {
            return false;
        }
        if (fl.getValueAsList().size() != fields.size()) {
            return false;
        }

        boolean equals = true;
        for (int i = 0; i < fl.getValueAsList().size(); i++) {
            if (!fl.getValueElement(i).equals(fields.get(i))) {
                equals = false;
                break;
            }
        }

        return equals == true && id.equals(fl.getId());
    }

    @Override
    public int hashCode() {
        int i = id.getValue();
        if (fields != null) {
            i |= fields.size();
            if (fields.size() > 0) {
                i ^= Arrays.hashCode(fields.toArray());
            }
        }
        return i;
    }

//    private void dump(AbstractField field, StringBuilder sb, int indent) {
//        String space = "    ";
//
//        for (int i = 0; i < indent; i++) {
//            sb.append(space);
//        }
//        sb.append(field.getType().name()).append(": ").append(field.getLabelString()).append("\n");
//
//        if (FieldType.Struct.equals(field.getType())) {
//            StructData sd = ((StructField) field).getValue();
//            for (int i = 0; i < indent; i++) {
//                sb.append(space);
//            }
//            sb.append(space).append("STRUCT: ").append(sd.getId()).append("\n");
//            if (null != sd.getValueAsList()) {
//                for (int i = 0; i < sd.getValueAsList().size(); i++) {
//                    sb.append(i);
//                    dump(sd.getValueElement(i), sb, indent + 2);
//                }
//            }
//        } else if (FieldType.List.equals(field.getType())) {
//            ListData ld = ((ListField) field).getValue();
//            for (int l = 0; l < ld.getValueAsList().size(); l++) {
//                StructData listItem = ld.getValueElement(l);
//                sb.append(l);
//                for (int i = 0; i < indent; i++) {
//                    sb.append(space);
//                }
//                sb.append(space).append("STRUCT: ").append(listItem.getId()).append("\n");
//
//                for (int x = 0; x < listItem.getValueAsList().size(); x++) {
////                    for (int y = 0; y < listItem.getValueAsList().size(); y++) {
//                    sb.append(l).append("-").append(x);
//                    dump(listItem.getValueElement(x), sb, indent + 2);
////                    }
//                }
//            }
//        }
//    }

    public void dump(PrintStream out) {
        int indent = 0;
        out.println("STRUCT: " + this.getId());
        for (AbstractField f : this.getValueAsList()) {
            f.printString(out, indent + 1);
        }
    }
}