/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tools.pixelprobe.decoder.psd;

import com.android.tools.chunkio.Chunk;
import com.android.tools.chunkio.Chunked;
import com.android.tools.pixelprobe.ColorMode;
import com.android.tools.pixelprobe.util.Strings;

import java.util.List;
import java.util.Map;

/**
 * This class describes the structure of a PSD file.
 * Read on to learn how a PSD file is stored on disk.
 */
@SuppressWarnings("unused")
@Chunked
final class PsdFile {
    @Chunk
    Header header;
    @Chunk
    ColorData colorData;
    @Chunk
    ImageResources resources;
    @Chunk
    LayersInformation layersInfo;
    @Chunk
    ImageData imageData;

    /**
     * PSD header. A few magic values and important information
     * about the image's dimensions, color depth and mode.
     */
    @Chunked
    static final class Header {
        // Magic marker
        @Chunk(byteCount = 4, match = "\"8BPS\"")
        String signature;

        // Version is always 1
        @Chunk(match = "1")
        short version;

        // 6 reserved bytes that must always be set to 0
        @Chunk(byteCount = 6)
        Void reserved;

        @Chunk(byteCount = 2)
        int channels;

        // Height comes before width here
        @Chunk
        int height;
        @Chunk
        int width;

        @Chunk
        short depth;
        // We only support RGB documents
        @Chunk(byteCount = 2)
        ColorMode colorMode;
    }

    /**
     * Only useful for indexed and duotone images.
     */
    @Chunked
    static final class ColorData {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "colorData.length")
        byte[] data;
    }

    /**
     * The image resources section is a mix-bag of a lot of stuff
     * (thumbnail, guides, printing data, EXIF, XMPP, etc.).
     * This section is divided into typed blocks.
     */
    @Chunked
    static final class ImageResources {
        @Chunk(byteCount = 4)
        long length;

        // Each block has a padded size to make it even
        // Specifying how many bytes we want to read upfront
        // ensures we'll be able to successfully read the rest
        // of the document
        @Chunk(dynamicByteCount = "imageResources.length", key = "imageResourceBlock.id")
        Map<Integer, ImageResourceBlock> blocks;
    }

    /**
     * An image resource block has a type (or ID) and an optional
     * name. We'll use the ID to find the blocks we want.
     */
    @Chunked
    static final class ImageResourceBlock {
        @Chunk(byteCount = 4, match = "\"8BIM\"")
        String signature;

        @Chunk(byteCount = 2)
        int id;

        // The name is stored as a Pascal string with even padding
        // The padding takes into account the length byte
        @Chunk(byteCount = 1)
        short nameLength;
        @Chunk(dynamicByteCount = "imageResourceBlock.nameLength")
        String name;
        @SuppressWarnings("unused")
        @Chunk(dynamicByteCount = "Math.max(1, imageResourceBlock.nameLength & 1)")
        Void padding;

        // The actual byte length of the block
        @Chunk(byteCount = 4)
        long length;

        // The length must be padded to make it even if we want to
        // successfully read a block. We could also add a Void pad after.
        @Chunk(dynamicByteCount = "imageResourceBlock.length + (imageResourceBlock.length & 1)",
            switchType = {
                @Chunk.Case(test = "imageResourceBlock.id == 0x0408", type = GuidesResourceBlock.class),
                @Chunk.Case(test = "imageResourceBlock.id == 0x040C", type = ThumbnailResourceBlock.class),
                @Chunk.Case(test = "imageResourceBlock.id == 0x03ED", type = ResolutionInfoBlock.class),
                @Chunk.Case(test = "imageResourceBlock.id == 0x040F", type = ColorProfileBlock.class),
                @Chunk.Case(test = "imageResourceBlock.id == 0x0416", type = UnsignedShortBlock.class),
                @Chunk.Case(test = "imageResourceBlock.id == 0x0417", type = UnsignedShortBlock.class)
            }
        )
        Object data;
    }

    /**
     * The layers information section contains the list of layers
     * and a lot of data for each layer.
     */
    @Chunked
    static final class LayersInformation {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(byteCount = 4)
        long listLength;

        @Chunk(dynamicByteCount = "layersInformation.listLength", readIf = "layersInformation.listLength > 0")
        LayersList layers;

        @Chunk(byteCount = 4)
        long globalMaskInfoLength;

        @Chunk(dynamicByteCount = "layersInformation.globalMaskInfoLength")
        Void globalMaskInfo;

        // Subtract 8 for listLength and globalMaskInfoLength
        @Chunk(dynamicByteCount =
                "layersInformation.length - layersInformation.listLength - layersInformation.globalMaskInfoLength - 8",
            key = "layerProperty.key")
        Map<String, LayerProperty> extras;
    }

    /**
     * The list of layers is actually made of two lists in the PSD
     * file. First, the description and extra data for each layer
     * (name, bounds, etc.). Then an image representation for each
     * layer, as a series of independently encoded channels.
     */
    @Chunked
    static final class LayersList {
        // The count can be negative, which means the first
        // alpha channel contains the transparency data for
        // the flattened image. This means we must ensure
        // we always take the absolute value of the layer
        // count to build lists
        @Chunk
        short count;

        @Chunk(dynamicSize = "Math.abs(layersList.count)")
        List<RawLayer> layers;

        @Chunk(dynamicSize = "Math.abs(layersList.count)")
        List<ChannelsContainer> channels;
    }

    /**
     * A layer contains a few static properties and a list of
     * keyed "extras", The extras are crucial for non-image
     * layers. They also contain layer effects (drop shadows,
     * inner glow, etc.).
     */
    @Chunked
    static final class RawLayer {
        // Mask for the flags field, indicating whether the layer is visible or not
        static final int INVISIBLE = 0x2;

        // Firs we have the layer's bounds, in pixels,
        // in absolute image coordinates
        @Chunk
        int top;
        @Chunk
        int left;
        @Chunk
        int bottom;
        @Chunk
        int right;

        // The channels count, 3 or 4 in our case since we
        // only support RGB files
        @Chunk(byteCount = 2)
        short channels;
        // Important stuff in there, read on
        @Chunk(dynamicSize = "rawLayer.channels")
        List<ChannelInformation> channelsInfo;

        @Chunk(byteCount = 4, match = "\"8BIM\"")
        String signature;
        @Chunk(byteCount = 4)
        String blendMode;

        // The opacity is stored as an unsigned byte,
        // from 0 (transparent) to 255 (opaque)
        @Chunk(byteCount = 1)
        short opacity;
        @Chunk
        byte clipping;
        @Chunk
        byte flags;

        // Padding gunk
        @Chunk(byteCount = 1)
        Void filler;

        // The number of bytes taken by all the extras
        @Chunk(byteCount = 4)
        long extraLength;

        @Chunk(dynamicByteCount = "rawLayer.extraLength")
        LayerExtras extras;
    }

    /**
     * Extremely important: this section gives us the number of bytes
     * used to encode the image data of each channel in a given layer.
     */
    @Chunked
    static final class ChannelInformation {
        @Chunk
        short id;
        @Chunk(byteCount = 4)
        long dataLength;
    }

    /**
     * The layer's extras contains important values such as
     * the layer's name and a series of named properties.
     */
    @Chunked
    static final class LayerExtras {
        @Chunk
        MaskAdjustment maskAdjustment;

        @Chunk(byteCount = 4)
        long blendRangesLength;

        // The first blend range is always composite gray
        @Chunk(dynamicByteCount = "layerExtras.blendRangesLength")
        List<BlendRange> layerBlendRanges;

        // The layer's name is stored as a Pascal string,
        // padded to a multiple of 4 bytes
        @Chunk(byteCount = 1)
        short nameLength;
        @Chunk(dynamicByteCount = "layerExtras.nameLength")
        String name;
        @Chunk(dynamicByteCount = "((layerExtras.nameLength + 4) & ~3) - (layerExtras.nameLength + 1)")
        Void namePadding;

        @Chunk(key = "layerProperty.key")
        Map<String, LayerProperty> properties;
    }

    /**
     * Specific to masks. This section has to be read carefully from the
     * file has its length varies depending on a set of flags.
     */
    @Chunked
    static final class MaskAdjustment {
        @Chunk(byteCount = 4, stopIf = "maskAdjustment.length == 0")
        long length;

        @Chunk(byteCount = 4)
        long top;
        @Chunk(byteCount = 4)
        long left;
        @Chunk(byteCount = 4)
        long bottom;
        @Chunk(byteCount = 4)
        long right;

        @Chunk(byteCount = 1)
        short defaultColor;

        @Chunk
        byte flags;

        @Chunk(readIf = "(maskAdjustment.flags & 0x10) != 0")
        byte maskParameters;

        @Chunk(byteCount = 1, readIf = "(maskAdjustment.maskParameters & 0x1) != 0")
        short userMaskDensity;
        @Chunk(readIf = "(maskAdjustment.maskParameters & 0x2) != 0")
        double userMaskFeather;
        @Chunk(byteCount = 1, readIf = "(maskAdjustment.maskParameters & 0x4) != 0")
        short vectorMaskDensity;
        @Chunk(readIf = "(maskAdjustment.maskParameters & 0x8) != 0")
        double vectorMaskFeather;

        @Chunk(readIf = "maskAdjustment.length == 20", stopIf = "maskAdjustment.length == 20")
        short padding;

        @Chunk
        byte realFlags;

        @Chunk(byteCount = 1)
        short userMaskBackground;

        @Chunk(byteCount = 4)
        long realTop;
        @Chunk(byteCount = 4)
        long realLeft;
        @Chunk(byteCount = 4)
        long realBottom;
        @Chunk(byteCount = 4)
        long realRight;
    }

    /**
     * A blend range indicates the tonal range for a given channel.
     */
    @Chunked
    static final class BlendRange {
        @Chunk(byteCount = 1)
        short srcBlackIn;
        @Chunk(byteCount = 1)
        short srcWhiteIn;
        @Chunk(byteCount = 1)
        short srcBlackOut;
        @Chunk(byteCount = 1)
        short srcWhiteOut;

        @Chunk(byteCount = 1)
        short dstBlackIn;
        @Chunk(byteCount = 1)
        short dstWhiteIn;
        @Chunk(byteCount = 1)
        short dstBlackOut;
        @Chunk(byteCount = 1)
        short dstWhiteOut;
    }

    /**
     * Layer properties encode a lot of interesting attributes but we will
     * only decode a few of them.
     */
    @Chunked
    static final class LayerProperty {
        // The property holds the layer's effects (drop shadows, etc.)
        static final String KEY_EFFECTS = "lfx2";
        // The property holds the layer's stacked effects (multiple
        // drop shadows, etc.) If this property is present, the
        // "lfx2" property above should not be present
        static final String KEY_MULTI_EFFECTS = "lmfx";
        // Indicates that this layer is a section (start or end of a layer group)
        static final String KEY_SECTION = "lsct";
        // The property holds the Unicode name of the layer
        static final String KEY_NAME = "luni";
        // The property holds the solid color adjustment information
        static final String KEY_ADJUSTMENT_SOLID_COLOR = "SoCo";
        // Fill opacity
        static final String KEY_FILL_OPACITY = "iOpa";
        // The property holds the text information (styles, text data, etc.)
        static final String KEY_TEXT = "TySh";
        // The property holds the vector data (can be "vsms" instead)
        static final String KEY_VECTOR_MASK = "vmsk";
        // The property holds the vector data (can be "vmsk" instead)
        // When this key is present, we must also look for "vscg"
        static final String KEY_SHAPE_MASK = "vsms";
        // The property holds graphics data for a shape mask defined by "vsms"
        static final String KEY_SHAPE_GRAPHICS = "vscg";
        // The property holds the stroke data
        static final String KEY_STROKE = "vstk";
        // The layer has a depth of 16 bit per channel
        static final String KEY_LAYER_DEPTH_16 = "Lr16";
        // The layer has a depth of 32 bit per channel
        static final String KEY_LAYER_DEPTH_32 = "Lr32";

        @Chunk(byteCount = 4)
        String signature;

        @Chunk(byteCount = 4)
        String key;

        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "(layerProperty.length + 3) & ~3",
            switchType = {
                @Chunk.Case(test = "layerProperty.key.equals(\"lmfx\")", type = LayerEffects.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"lfx2\")", type = LayerEffects.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"lsct\")", type = LayerSection.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"luni\")", type = UnicodeString.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"SoCo\")", type = SolidColorAdjustment.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"iOpa\")", type = byte.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"TySh\")", type = TypeToolObject.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"vmsk\")", type = ShapeMask.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"vsms\")", type = ShapeMask.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"vscg\")", type = ShapeGraphics.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"vstk\")", type = ShapeStroke.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"Lr16\")", type = LayersList.class),
                @Chunk.Case(test = "layerProperty.key.equals(\"Lr32\")", type = LayersList.class),
            }
        )
        Object data;
    }

    @Chunked
    static final class LayerEffects {
        // Boolean to toggle effects on/off
        static final String KEY_MASTER_SWITCH = "masterFXSwitch";
        static final String KEY_PRESENT = "present";
        static final String KEY_ENABLED = "enab";
        // Shadows
        static final String KEY_INNER_SHADOW = "IrSh";
        static final String KEY_INNER_SHADOW_MULTI = "innerShadowMulti";
        static final String KEY_DROP_SHADOW = "DrSh";
        static final String KEY_DROP_SHADOW_MULTI = "dropShadowMulti";
        // Shape stroke
        static final String KEY_STROKE = "FrFX";

        @Chunk(match = "0")
        int version;
        @Chunk
        int descriptorVersion;

        @Chunk
        Descriptor effects;
    }

    /**
     * A layer section is a property that marks layer groups.
     */
    @Chunked
    static final class LayerSection {
        /**
         * The section type.
         */
        enum Type {
            /**
             * Who knows.
             */
            OTHER,
            /**
             * Open group of layers.
             */
            GROUP_OPENED,
            /**
             * Closed group of layers.
             */
            GROUP_CLOSED,
            /**
             * End of a group (invisible in the UI).
             */
            BOUNDING
        }

        @Chunk(byteCount = 4)
        Type type;

        @Chunk(byteCount = 4,
            readIf = "(($T) stack.get(1)).length >= 12",
            readIfParams = { LayerProperty.class }
        )
        String signature;

        @Chunk(byteCount = 4,
            readIf = "(($T) stack.get(1)).length >= 12",
            readIfParams = { LayerProperty.class }
        )
        String blendMode;

        // Apparently only used for the animation timeline
        @Chunk(byteCount = 4,
            readIf = "(($T) stack.get(1)).length >= 16",
            readIfParams = { LayerProperty.class }
        )
        int subType;
    }

    @Chunked
    static final class SolidColorAdjustment {
        @Chunk
        int version;

        @Chunk
        Descriptor solidColor;
    }

    /**
     * The TypeToolObject layer property contains all the data needed to
     * render a text layer.
     */
    @Chunked
    static final class TypeToolObject {
        // Descriptor that holds the actual text
        static final String KEY_TEXT = "Txt ";
        // Descriptor that holds structured text data used for styling, see TextEngine
        static final String KEY_ENGINE_DATA = "EngineData";

        @Chunk
        short version;

        // The text's transform (translation, scale and shear)
        @Chunk
        double xx;
        @Chunk
        double xy;
        @Chunk
        double yx;
        @Chunk
        double yy;
        @Chunk
        double tx;
        @Chunk
        double ty;

        @Chunk
        short textVersion;

        @Chunk
        int testDescriptorVersion;

        // The descriptor is a horrifyingly generic object
        // that happens to hold important data (the actual
        // text, styling info, etc.)
        @Chunk
        Descriptor text;

        @Chunk
        short warpVersion;

        @Chunk
        int warpDescriptorVersion;

        @Chunk
        Descriptor warp;

        // These always seem to be set to 0
        @Chunk
        int left;
        @Chunk
        int top;
        @Chunk
        int right;
        @Chunk
        int bottom;
    }

    /**
     * A vector mask is a layer property that contains a list of path
     * records, used to describe a path (or vector shape).
     */
    @Chunked
    static final class ShapeMask {
        @Chunk
        int version;

        @Chunk
        int flags;

        // LayerProperty.length is rounded to an even byte count
        // Subtract the 8 bytes used for version and flags, and divide
        // by the length of each path record (26 bytes) to know how many
        // path records to read
        @Chunk(dynamicSize = "(int) Math.floor(((($T) stack.get(1)).length - 8) / 26)",
               sizeParams = { LayerProperty.class })
        List<PathRecord> pathRecords;
    }

    /**
     * A shape graphics object describes all the graphics properties of
     * a shape mask (color, gradient, etc.)
     */
    @Chunked
    static final class ShapeGraphics {
        @Chunk(byteCount =  4)
        String key;

        @Chunk(byteCount = 4)
        long version;

        @Chunk
        Descriptor graphics;
    }

    /**
     * Describes a shape layer's stroke properties.
     */
    @Chunked
    static final class ShapeStroke {
        @Chunk(byteCount = 4)
        long version;

        @Chunk
        Descriptor stroke;
    }

    /**
     * A path records is either a single point in a path, a subpath
     * marker or a command. A subpath can be closed or open.
     */
    @Chunked
    static final class PathRecord {
        // Marks the beginning of a closed subpath
        static final int CLOSED_SUBPATH_LENGTH = 0;
        // Linked/unlinked matters only to interactive editing
        // Linked just means that the two control points of a knot
        // move together when one of them moves
        static final int CLOSED_SUBPATH_KNOT_LINKED = 1;
        static final int CLOSED_SUBPATH_KNOT_UNLINKED = 2;
        // Marks the beginning of an open subpath
        static final int OPEN_SUBPATH_LENGTH = 3;
        static final int OPEN_SUBPATH_KNOT_LINKED = 4;
        static final int OPEN_SUBPATH_KNOT_UNLINKED = 5;
        // Photoshop only deal with even/odd fill rules, we can ignore it
        static final int PATH_FILL_RULE = 6;
        // Not sure what this does
        static final int CLIPBOARD = 7;
        // Initial fill rule, always present as first item in the list
        // of path records
        static final int INITIAL_FILL_RULE = 8;

        /**
         * A curve (or path) is made of a series of Bézier knot.
         * Each knot is made of an anchor (point on the curve/path)
         * and of two control points. One defines the slope of the
         * curve before the anchor, the other the slope of the curve
         * after the anchor.
         */
        @Chunked
        static final class BezierKnot {
            @Chunk
            int controlEnterY;
            @Chunk
            int controlEnterX;
            @Chunk
            int anchorY;
            @Chunk
            int anchorX;
            @Chunk
            int controlExitY;
            @Chunk
            int controlExitX;
        }

        /**
         * Marks the start of a sub-path. The first command of a
         * sub-path is always a "move to". While sub-paths could
         * all be recorded in a single path, we must take into
         * account Photoshop's operators: merge, subtract,
         * intersect and XOR). We therefore record the sub-paths
         * individually to be able to apply the operators
         * properly later on.
         */
        @Chunked
        static final class SubPath {
            // If the tag equals this value, then the subpath is
            // not a path operation and must be added to the
            // current path
            static final int NO_OP = 0x0;
            static final int OP_XOR = 0x0;
            static final int OP_MERGE = 0x1;
            static final int OP_SUBTRACT = 0x2;
            static final int OP_INTERSECT = 0x3;

            @Chunk(byteCount = 2)
            int knotCount;

            @Chunk(byteCount = 2)
            int op;

            @Chunk(byteCount = 2)
            int tag;
        }

        // Indicates the path record type
        @Chunk
        short selector;

        @Chunk(byteCount = 24,
            switchType = {
                @Chunk.Case(test = "pathRecord.selector == 0 || pathRecord.selector == 3",
                            type = SubPath.class),
                @Chunk.Case(test = "pathRecord.selector == 1 || pathRecord.selector == 2 || " +
                                   "pathRecord.selector == 4 || pathRecord.selector == 5",
                            type = BezierKnot.class)
            }
        )
        Object data;
    }

    /**
     * Contains the image data for each channel of a layer.
     * There is one ChannelsContainer per layer.
     */
    @Chunked
    static final class ChannelsContainer {
        @Chunk(
            dynamicSize =
                "$T list = ($T) stack.get(1);\n" +
                "size = list.layers.get(list.channels.size()).channels",
            sizeParams = {
                LayersList.class,
                LayersList.class
            }
        )
        List<ChannelImageData> imageData;
    }

    /**
     * The image data for a layer's channel. The compression method can
     * in theory vary per layer. The overall length of the data comes from
     * the ChannelInformation section seen earlier.
     */
    @Chunked
    static final class ChannelImageData {
        @Chunk(byteCount = 2)
        CompressionMethod compression;

        // Subtract 2 bytes because the channel info data length takes the
        // compression method into account
        @Chunk(
            dynamicByteCount =
                "$T list = ($T) stack.get(2);\n" +
                "$T layer = list.layers.get(list.channels.size());\n" +
                "$T container = ($T) stack.get(1);\n" +
                "$T info = layer.channelsInfo.get(container.imageData.size());\n" +
                "byteCount = info.dataLength - 2",
            byteCountParams = {
                LayersList.class, LayersList.class,
                RawLayer.class,
                ChannelsContainer.class, ChannelsContainer.class,
                ChannelInformation.class
            }
        )
        byte[] data;
    }

    /**
     * Photoshop's compression method for layer channels and
     * the flattened planar image.
     */
    enum CompressionMethod {
        RAW,
        RLE,
        ZIP,
        ZIP_NO_PREDICTION
    }

    /**
     * The flattened image data. The compression method applies to all
     * the channels. The channels are stored sequentially in the data array.
     */
    @Chunked
    static final class ImageData {
        @Chunk(byteCount = 2)
        CompressionMethod compression;

        // Use a 128K buffer because images can be large
        // We might want to choose a buffer size that's function of
        // the dimensions/compression method
        @Chunk(bufferSize = 0x20000)
        byte[] data;
    }

    /**
     * A descriptor is a generic way to represent typed and named
     * data. Photoshop seems to abuse descriptor quite a bit in
     * few places, particularly for text layers.
     */
    @Chunked
    static final class Descriptor {
        static final String CLASS_ID_COLOR_RGB = "RGBC";
        static final String CLASS_ID_COLOR_HSB = "HSBC";
        static final String CLASS_ID_COLOR_CMYK = "CMYC";
        static final String CLASS_ID_COLOR_LAB = "LbCl";
        static final String CLASS_ID_COLOR_GRAY = "Grsc";

        // Name from classID, usually not set
        @Chunk
        UnicodeString name;

        // ClassID
        @Chunk
        MinimumString classId;

        // Number of items in the descriptor
        @Chunk
        int count;

        @Chunk(dynamicSize = "descriptor.count", key = "String.valueOf(descriptorItem.key)")
        Map<String, DescriptorItem> items;

        @Override
        public String toString() {
            return "<descriptor name=\"" + name + "\" classId=\"" + classId + "\">" +
                   Strings.join(items.values(), "\n") +
                   "</descriptor>";
        }
    }

    /**
     * A descriptor item has a name and a value.
     * The value itself is typed.
     */
    @Chunked
    static final class DescriptorItem {
        /**
         * Enum reference (see Reference descriptor item)
         */
        @Chunked
        static final class Enumerated {
            @Chunk
            MinimumString type;
            @Chunk
            MinimumString value;

            @Override
            public String toString() {
                return String.valueOf(value);
            }
        }

        /**
         * A double value with a unit.
         */
        @Chunked
        static final class UnitDouble {
            static final String POINTS = "#Pnt";
            static final String MILLIMETERS = "#Mlm";
            static final String CENTIMETERS = "RrCm";
            static final String INCHES = "RrIn";
            static final String ANGLE_DEGREES = "#Ang";
            static final String RESOLUTION = "#Rsl"; // base per inch
            static final String RELATIVE = "#Rlt"; // base 72ppi
            static final String NONE = "#Nne";
            static final String PERCENT = "#Prc";
            static final String PIXELS = "#Pxl";

            @Chunk(byteCount = 4)
            String unit;

            @Chunk
            double value;

            @Override
            public String toString() {
                String s = Double.toString(value);
                switch (unit) {
                    case POINTS:
                        s += "pt";
                        break;
                    case MILLIMETERS:
                        s += "mm";
                        break;
                    case CENTIMETERS:
                        s += "cm";
                        break;
                    case INCHES:
                        s += "in";
                        break;
                    case ANGLE_DEGREES:
                        s += "°";
                        break;
                    case RESOLUTION:
                        s += "dpi";
                        break;
                    case RELATIVE:
                        s += "dpp";
                        break;
                    case NONE:
                        break;
                    case PERCENT:
                        s += "%";
                        break;
                    case PIXELS:
                        s += "px";
                        break;
                }
                return s;
            }
        }

        /**
         * A float value with a unit.
         * See {@link UnitDouble} for possible units.
         */
        @Chunked
        static final class UnitFloat {
            @Chunk(byteCount = 4)
            String unit;

            @Chunk
            float value;

            @Override
            public String toString() {
                return Float.toString(value);
            }
        }

        /**
         * A class name + class ID. Used in references.
         */
        @Chunked
        static final class ClassType {
            @Chunk
            UnicodeString name;
            @Chunk
            MinimumString classId;
        }

        /**
         * A property reference.
         */
        @Chunked
        static final class Property {
            @Chunk
            ClassType classType;
            @Chunk
            MinimumString keyId;
        }

        /**
         * A reference is actually a list of items.
         * Hard to say why.
         */
        @Chunked
        static final class Reference {
            @Chunked
            static final class Item {
                @Chunk(byteCount = 4)
                String type;

                @Chunk(switchType = {
                    @Chunk.Case(test = "item.type.equals(\"Enmr\")", type = Enumerated.class),
                    @Chunk.Case(test = "item.type.equals(\"Clss\")", type = ClassType.class),
                    @Chunk.Case(test = "item.type.equals(\"Idnt\")", type = int.class),
                    @Chunk.Case(test = "item.type.equals(\"indx\")", type = int.class),
                    @Chunk.Case(test = "item.type.equals(\"name\")", type = UnicodeString.class),
                    @Chunk.Case(test = "item.type.equals(\"prop\")", type = Property.class),
                    @Chunk.Case(test = "item.type.equals(\"rele\")", type = int.class)
                })
                Object data;
            }

            @Chunk
            int count;

            @Chunk(dynamicSize = "reference.count")
            List<Item> items;
        }

        /**
         * Holds a list of descriptor values.
         */
        @Chunked
        static final class ValueList {
            @Chunk
            int count;

            @Chunk(dynamicSize = "valueList.count")
            List<Value> items;

            @Override
            public String toString() {
                return "<list>" + Strings.join(items, ", ") + "</list>";
            }
        }

        /**
         * The value of a descriptor. A value can be one of many types,
         * see the annotation below to find out the exact list.
         */
        @Chunked
        static final class Value {
            @Chunk(byteCount = 4)
            String type;

            @Chunk(switchType = {
                @Chunk.Case(test = "value.type.equals(\"alis\")", type = FixedString.class),
                @Chunk.Case(test = "value.type.equals(\"bool\")", type = boolean.class),
                @Chunk.Case(test = "value.type.equals(\"comp\")", type = long.class),
                @Chunk.Case(test = "value.type.equals(\"doub\")", type = double.class),
                @Chunk.Case(test = "value.type.equals(\"enum\")", type = Enumerated.class),
                @Chunk.Case(test = "value.type.equals(\"GlbC\")", type = ClassType.class),
                @Chunk.Case(test = "value.type.equals(\"GlbO\")", type = Descriptor.class),
                @Chunk.Case(test = "value.type.equals(\"long\")", type = int.class),
                @Chunk.Case(test = "value.type.equals(\"obj\" )", type = Reference.class),
                @Chunk.Case(test = "value.type.equals(\"Objc\")", type = Descriptor.class),
                @Chunk.Case(test = "value.type.equals(\"TEXT\")", type = UnicodeString.class),
                @Chunk.Case(test = "value.type.equals(\"tdta\")", type = FixedByteArray.class),
                @Chunk.Case(test = "value.type.equals(\"type\")", type = ClassType.class),
                @Chunk.Case(test = "value.type.equals(\"UnFl\")", type = UnitFloat.class),
                @Chunk.Case(test = "value.type.equals(\"UntF\")", type = UnitDouble.class),
                @Chunk.Case(test = "value.type.equals(\"VlLs\")", type = ValueList.class)
            })
            Object data;

            @Override
            public String toString() {
                if (data == null) return null;
                return data.toString();
            }
        }

        // The name of the descriptor
        @Chunk
        MinimumString key;

        @Chunk
        Value value;

        @Override
        public String toString() {
            return "<item key=\"" + key + "\">" + String.valueOf(value) + "</item>";
        }
    }

    /**
     * Stores the documents guides. Pretty straightforward.
     */
    @Chunked
    static final class GuidesResourceBlock {
        static final int ID = 0x0408;

        @Chunk
        int version;

        // Reserved chunk of 8 bytes. Ignore.
        @Chunk(byteCount = 8)
        Void future;

        @Chunk
        int guideCount;

        @Chunk(dynamicSize = "guidesResourceBlock.guideCount")
        List<GuideBlock> guides;
    }

    /**
     * Orientation, used for guides.
     */
    enum Orientation {
        VERTICAL,
        HORIZONTAL
    }

    /**
     * An actual guide. It has a location (fixed-point 27.5 format)
     * and an orientation.
     */
    @Chunked
    static final class GuideBlock {
        @Chunk
        int location;

        @Chunk(byteCount = 1)
        Orientation orientation;
    }

    /**
     * Stores an ICC color profile.
     */
    @Chunked
    static final class ColorProfileBlock {
        static final int ID = 0x040F;

        @Chunk
        byte[] icc;
    }

    /**
     * Resolution units for the resolution info block.
     */
    enum ResolutionUnit {
        UNKNOWN,
        PIXEL_PER_INCH,
        PIXEL_PER_CM
    }

    /**
     * Display units for the resolution info block.
     * We don't really care about this unit.
     */
    enum DisplayUnit {
        UNKNOWN,
        INCHES,
        CENTIMETERS,
        POINTS,
        PICAS,
        COLUMNS
    }

    /**
     * Stores the document's vertical and horizontal resolution
     * as well as information on how to display dimensions in the UI.
     */
    @Chunked
    static final class ResolutionInfoBlock {
        static final int ID = 0x03ED;

        @Chunk
        int horizontalResolution;
        @Chunk(byteCount = 2)
        ResolutionUnit horizontalUnit;
        @Chunk(byteCount = 2)
        DisplayUnit widthUnit;

        @Chunk
        int verticalResolution;
        @Chunk(byteCount = 2)
        ResolutionUnit verticalUnit;
        @Chunk(byteCount = 2)
        DisplayUnit heightUnit;
    }

    /**
     * Thumbnails can be stored as RAW data or JPEGs.
     * We currently only support the JPEG format.
     */
    @Chunked
    static final class ThumbnailResourceBlock {
        static final int ID = 0x040C;

        // RAW=0, JPEG=1, we only want JPEG
        @Chunk(match = "1")
        int format;

        @Chunk(byteCount = 4)
        long width;
        @Chunk(byteCount = 4)
        long height;

        @Chunk(byteCount = 4)
        long rowBytes;
        @Chunk(byteCount = 4)
        long size;
        @Chunk(byteCount = 4)
        long compressedSize;

        // JPEG guarantees 24bpp and 1 plane
        @Chunk(match = "24")
        short bpp;
        @Chunk(match = "1")
        short planes;

        @Chunk(dynamicByteCount = "thumbnailResourceBlock.compressedSize")
        byte[] thumbnail;
    }

    /**
     * Stores an unsigned short.
     */
    @Chunked
    static final class UnsignedShortBlock {
        static final int ID_INDEX_TABLE_COUNT = 0x0416;
        static final int ID_INDEX_TRANSPARENCY = 0x0417;

        @Chunk(byteCount = 2)
        int data;
    }

    /**
     * Helper class to read a byte array whose length is stored
     * as a 32 bits unsigned integer.
     */
    @Chunked
    static final class FixedByteArray {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "fixedByteArray.length")
        byte[] value;

        @Override
        public String toString() {
            if (value == null) return null;
            return new String(value);
        }
    }

    /**
     * Helper class to read a Unicode string, encoded in UTF-16.
     * The length, in characters, of the string is stored as a 32
     * bits unsigned integer. The string is made of length*2 bytes.
     */
    @Chunked
    static final class UnicodeString {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "unicodeString.length * 2", encoding = "UTF-16")
        String value;

        @Override
        public String toString() {
            if (value == null) return null;
            if (value.length() == 0) return "";
            int lastChar = value.length() - 1;
            if (value.charAt(lastChar) == '\0') {
                return value.substring(0, lastChar);
            }
            return value;
        }
    }

    /**
     * Helper class to read an ASCII string whose length is
     * stored as a 32 bits unsigned integer. If the length
     * is 0, the string is assumed to have a length of 4 bytes.
     */
    @Chunked
    static final class MinimumString {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "Math.max(minimumString.length, 4)")
        String value;

        @Override
        public String toString() {
            return value;
        }
    }

    /**
     * Helper class to read an ASCII string whose length is
     * stored as a 32 bits unsigned integer.
     */
    @Chunked
    static final class FixedString {
        @Chunk(byteCount = 4)
        long length;

        @Chunk(dynamicByteCount = "fixedString.length")
        String value;

        @Override
        public String toString() {
            return value;
        }
    }
}
