package com.jme3.scene.plugins.blender.file;

import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.file.Structure.DataType;

import java.util.ArrayList;
import java.util.List;

/**
 * This class represents a single field in the structure. It can be either a primitive type or a table or a reference to
 * another structure.
 * @author Marcin Roguski
 */
/* package */
class Field implements Cloneable {

    private static final int NAME_LENGTH = 24;
    private static final int TYPE_LENGTH = 16;
    /** The blender context. */
    public BlenderContext    blenderContext;
    /** The type of the field. */
    public String            type;
    /** The name of the field. */
    public String            name;
    /** The value of the field. Filled during data reading. */
    public Object            value;
    /** This variable indicates the level of the pointer. */
    public int               pointerLevel;
    /**
     * This variable determines the sizes of the array. If the value is null the n the field is not an array.
     */
    public int[]             tableSizes;
    /** This variable indicates if the field is a function pointer. */
    public boolean           function;

    /**
     * Constructor. Saves the field data and parses its name.
     * @param name
     *            the name of the field
     * @param type
     *            the type of the field
     * @param blenderContext
     *            the blender context
     * @throws BlenderFileException
     *             this exception is thrown if the names contain errors
     */
    public Field(String name, String type, BlenderContext blenderContext) throws BlenderFileException {
        this.type = type;
        this.blenderContext = blenderContext;
        this.parseField(new StringBuilder(name));
    }

    /**
     * Copy constructor. Used in clone method. Copying is not full. The value in the new object is not set so that we
     * have a clead empty copy of the filed to fill with data.
     * @param field
     *            the object that we copy
     */
    private Field(Field field) {
        type = field.type;
        name = field.name;
        blenderContext = field.blenderContext;
        pointerLevel = field.pointerLevel;
        if (field.tableSizes != null) {
            tableSizes = field.tableSizes.clone();
        }
        function = field.function;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return new Field(this);
    }

    /**
     * This method fills the field wth data read from the input stream.
     * @param blenderInputStream
     *            the stream we read data from
     * @throws BlenderFileException
     *             an exception is thrown when the blend file is somehow invalid or corrupted
     */
    public void fill(BlenderInputStream blenderInputStream) throws BlenderFileException {
        int dataToRead = 1;
        if (tableSizes != null && tableSizes.length > 0) {
            for (int size : tableSizes) {
                if (size <= 0) {
                    throw new BlenderFileException("The field " + name + " has invalid table size: " + size);
                }
                dataToRead *= size;
            }
        }
        DataType dataType = pointerLevel == 0 ? DataType.getDataType(type, blenderContext) : DataType.POINTER;
        switch (dataType) {
            case POINTER:
                if (dataToRead == 1) {
                    Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
                    pointer.fill(blenderInputStream);
                    value = pointer;
                } else {
                    Pointer[] data = new Pointer[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        Pointer pointer = new Pointer(pointerLevel, function, blenderContext);
                        pointer.fill(blenderInputStream);
                        data[i] = pointer;
                    }
                    value = new DynamicArray<Pointer>(tableSizes, data);
                }
                break;
            case CHARACTER:
                // character is also stored as a number, because sometimes the new blender version uses
                // other number type instead of character as a field type
                // and characters are very often used as byte number stores instead of real chars
                if (dataToRead == 1) {
                    value = Byte.valueOf((byte) blenderInputStream.readByte());
                } else {
                    Character[] data = new Character[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Character.valueOf((char) blenderInputStream.readByte());
                    }
                    value = new DynamicArray<Character>(tableSizes, data);
                }
                break;
            case SHORT:
                if (dataToRead == 1) {
                    value = Integer.valueOf(blenderInputStream.readShort());
                } else {
                    Number[] data = new Number[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Integer.valueOf(blenderInputStream.readShort());
                    }
                    value = new DynamicArray<Number>(tableSizes, data);
                }
                break;
            case INTEGER:
                if (dataToRead == 1) {
                    value = Integer.valueOf(blenderInputStream.readInt());
                } else {
                    Number[] data = new Number[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Integer.valueOf(blenderInputStream.readInt());
                    }
                    value = new DynamicArray<Number>(tableSizes, data);
                }
                break;
            case LONG:
                if (dataToRead == 1) {
                    value = Long.valueOf(blenderInputStream.readLong());
                } else {
                    Number[] data = new Number[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Long.valueOf(blenderInputStream.readLong());
                    }
                    value = new DynamicArray<Number>(tableSizes, data);
                }
                break;
            case FLOAT:
                if (dataToRead == 1) {
                    value = Float.valueOf(blenderInputStream.readFloat());
                } else {
                    Number[] data = new Number[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Float.valueOf(blenderInputStream.readFloat());
                    }
                    value = new DynamicArray<Number>(tableSizes, data);
                }
                break;
            case DOUBLE:
                if (dataToRead == 1) {
                    value = Double.valueOf(blenderInputStream.readDouble());
                } else {
                    Number[] data = new Number[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        data[i] = Double.valueOf(blenderInputStream.readDouble());
                    }
                    value = new DynamicArray<Number>(tableSizes, data);
                }
                break;
            case VOID:
                break;
            case STRUCTURE:
                if (dataToRead == 1) {
                    Structure structure = blenderContext.getDnaBlockData().getStructure(type);
                    structure.fill(blenderContext.getInputStream());
                    value = structure;
                } else {
                    Structure[] data = new Structure[dataToRead];
                    for (int i = 0; i < dataToRead; ++i) {
                        Structure structure = blenderContext.getDnaBlockData().getStructure(type);
                        structure.fill(blenderContext.getInputStream());
                        data[i] = structure;
                    }
                    value = new DynamicArray<Structure>(tableSizes, data);
                }
                break;
            default:
                throw new IllegalStateException("Unimplemented filling of type: " + type);
        }
    }

    /**
     * This method parses the field name to determine how the field should be used.
     * @param nameBuilder
     *            the name of the field (given as StringBuilder)
     * @throws BlenderFileException
     *             this exception is thrown if the names contain errors
     */
    private void parseField(StringBuilder nameBuilder) throws BlenderFileException {
        this.removeWhitespaces(nameBuilder);
        // veryfying if the name is a pointer
        int pointerIndex = nameBuilder.indexOf("*");
        while (pointerIndex >= 0) {
            ++pointerLevel;
            nameBuilder.deleteCharAt(pointerIndex);
            pointerIndex = nameBuilder.indexOf("*");
        }
        // veryfying if the name is a function pointer
        if (nameBuilder.indexOf("(") >= 0) {
            function = true;
            this.removeCharacter(nameBuilder, '(');
            this.removeCharacter(nameBuilder, ')');
        } else {
            // veryfying if the name is a table
            int tableStartIndex = 0;
            List<Integer> lengths = new ArrayList<Integer>(3);// 3 dimensions will be enough in most cases
            do {
                tableStartIndex = nameBuilder.indexOf("[");
                if (tableStartIndex > 0) {
                    int tableStopIndex = nameBuilder.indexOf("]");
                    if (tableStopIndex < 0) {
                        throw new BlenderFileException("Invalid structure name: " + name);
                    }
                    try {
                        lengths.add(Integer.valueOf(nameBuilder.substring(tableStartIndex + 1, tableStopIndex)));
                    } catch (NumberFormatException e) {
                        throw new BlenderFileException("Invalid structure name caused by invalid table length: " + name, e);
                    }
                    nameBuilder.delete(tableStartIndex, tableStopIndex + 1);
                }
            } while (tableStartIndex > 0);
            if (!lengths.isEmpty()) {
                tableSizes = new int[lengths.size()];
                for (int i = 0; i < tableSizes.length; ++i) {
                    tableSizes[i] = lengths.get(i).intValue();
                }
            }
        }
        name = nameBuilder.toString();
    }

    /**
     * This method removes the required character from the text.
     * @param text
     *            the text we remove characters from
     * @param toRemove
     *            the character to be removed
     */
    private void removeCharacter(StringBuilder text, char toRemove) {
        for (int i = 0; i < text.length(); ++i) {
            if (text.charAt(i) == toRemove) {
                text.deleteCharAt(i);
                --i;
            }
        }
    }

    /**
     * This method removes all whitespaces from the text.
     * @param text
     *            the text we remove whitespaces from
     */
    private void removeWhitespaces(StringBuilder text) {
        for (int i = 0; i < text.length(); ++i) {
            if (Character.isWhitespace(text.charAt(i))) {
                text.deleteCharAt(i);
                --i;
            }
        }
    }

    /**
     * This method builds the full name of the field (with function, pointer and table indications).
     * @return the full name of the field
     */
    /*package*/ String getFullName() {
        StringBuilder result = new StringBuilder();
        if (function) {
            result.append('(');
        }
        for (int i = 0; i < pointerLevel; ++i) {
            result.append('*');
        }
        result.append(name);
        if (tableSizes != null) {
            for (int i = 0; i < tableSizes.length; ++i) {
                result.append('[').append(tableSizes[i]).append(']');
            }
        }
        if (function) {
            result.append(")()");
        }
        return result.toString();
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append(this.getFullName());

        // insert appropriate amount of spaces to format the output corrently
        int nameLength = result.length();
        result.append(' ');// at least one space is a must
        for (int i = 1; i < NAME_LENGTH - nameLength; ++i) {// we start from i=1 because one space is already added
            result.append(' ');
        }
        result.append(type);
        nameLength = result.length();
        for (int i = 0; i < NAME_LENGTH + TYPE_LENGTH - nameLength; ++i) {
            result.append(' ');
        }
        if (value instanceof Character) {
            result.append(" = ").append((int) ((Character) value).charValue());
        } else {
            result.append(" = ").append(value != null ? value.toString() : "null");
        }
        return result.toString();
    }
}