package com.shazam.axmlparser;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

public class StringBlock {

    /**
     * Reads whole (including chunk type) string block from stream.
     * Stream must be at the chunk type.
     *
     * @param reader the int reader
     * @return the string block from stream
     * @throws IOException when string or style data size is not multiple of 4
     */
    public static StringBlock read(ExtDataInput reader) throws IOException {
        reader.skipCheckChunkTypeInt(CHUNK_STRINGPOOL_TYPE, CHUNK_NULL_TYPE);
        int chunkSize = reader.readInt();

        // ResStringPool_header
        int stringCount = reader.readInt();
        int styleCount = reader.readInt();
        int flags = reader.readInt();
        int stringsOffset = reader.readInt();
        int stylesOffset = reader.readInt();

        StringBlock block = new StringBlock();
        block.m_isUTF8 = (flags & UTF8_FLAG) != 0;
        block.m_stringOffsets = reader.readIntArray(stringCount);
        block.m_stringOwns = new int[stringCount];
        Arrays.fill(block.m_stringOwns, -1);

        if (styleCount != 0) {
            block.m_styleOffsets = reader.readIntArray(styleCount);
        }

        int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset;
        block.m_strings = new byte[size];
        reader.readFully(block.m_strings);

        if (stylesOffset != 0) {
            size = (chunkSize - stylesOffset);
            block.m_styles = reader.readIntArray(size / 4);

            // read remaining bytes
            int remaining = size % 4;
            if (remaining >= 1) {
                while (remaining-- > 0) {
                    reader.readByte();
                }
            }
        }

        return block;
    }

    /**
     * Returns number of strings in block.
     *
     * @return the number of strings
     */
    public int getCount() {
        return m_stringOffsets != null ? m_stringOffsets.length : 0;
    }

    /**
     * Gets raw string at index
     *
     * @param index the index
     * @return raw string (without any styling information) at specified index or null if index is invalid or object was not initialized.
     */
    public String getString(int index) {
        if (index < 0 || m_stringOffsets == null || index >= m_stringOffsets.length) {
            return null;
        }
        int offset = m_stringOffsets[index];
        int length;

        if (m_isUTF8) {
            int[] val = getUtf8(m_strings, offset);
            offset = val[0];
            length = val[1];
        } else {
            int[] val = getUtf16(m_strings, offset);
            offset += val[0];
            length = val[1];
        }
        return decodeString(offset, length);
    }

    /**
     * Gets string with style information
     *
     * @param index the index
     * @return string with style information or null if index is invalid or object was not initialized.
     */
    public CharSequence get(int index) {
        return getString(index);
    }

    /**
     * Returns string with style tags (html-like).
     *
     * @param index the index of style tags
     * @return the style tag
     */
    public String getHTML(int index) {
        String raw = getString(index);
        if (raw == null) {
            return raw;
        }
        int[] style = getStyle(index);
        if (style == null) {
            return ResXmlEncoders.escapeXmlChars(raw);
        }

        // If the returned style is further in string, than string length. Lets skip it.
        if (style[1] > raw.length()) {
            return ResXmlEncoders.escapeXmlChars(raw);
        }
        StringBuilder html = new StringBuilder(raw.length() + 32);
        int[] opened = new int[style.length / 3];
        boolean[] unclosed = new boolean[style.length / 3];
        int offset = 0, depth = 0;
        while (true) {
            int i = -1, j;
            for (j = 0; j != style.length; j += 3) {
                if (style[j + 1] == -1) {
                    continue;
                }
                if (i == -1 || style[i + 1] > style[j + 1]) {
                    i = j;
                }
            }
            int start = ((i != -1) ? style[i + 1] : raw.length());
            for (j = depth - 1; j >= 0; j--) {
                int last = opened[j];
                int end = style[last + 2];
                if (end >= start) {
                    if (style[last + 1] == -1 && end != -1) {
                        unclosed[j] = true;
                    }
                    break;
                }
                if (offset <= end) {
                    html.append(ResXmlEncoders.escapeXmlChars(raw.substring(offset, end + 1)));
                    offset = end + 1;
                }
                outputStyleTag(getString(style[last]), html, true);
            }
            depth = j + 1;
            if (offset < start) {
                html.append(ResXmlEncoders.escapeXmlChars(raw.substring(offset, start)));
                if (j >= 0 && unclosed.length >= j && unclosed[j]) {
                    if (unclosed.length > (j + 1) && unclosed[j + 1] || unclosed.length == 1) {
                        outputStyleTag(getString(style[opened[j]]), html, true);
                    }
                }
                offset = start;
            }
            if (i == -1) {
                break;
            }
            outputStyleTag(getString(style[i]), html, false);
            style[i + 1] = -1;
            opened[depth++] = i;
        }
        return html.toString();
    }

    private void outputStyleTag(String tag, StringBuilder builder, boolean close) {
        builder.append('<');
        if (close) {
            builder.append('/');
        }

        int pos = tag.indexOf(';');
        if (pos == -1) {
            builder.append(tag);
        } else {
            builder.append(tag.substring(0, pos));
            if (!close) {
                boolean loop = true;
                while (loop) {
                    int pos2 = tag.indexOf('=', pos + 1);

                    // malformed style information will cause crash. so
                    // prematurely end style tags, if recreation
                    // cannot be created.
                    if (pos2 != -1) {
                        builder.append(' ').append(tag.substring(pos + 1, pos2)).append("=\"");
                        pos = tag.indexOf(';', pos2 + 1);

                        String val;
                        if (pos != -1) {
                            val = tag.substring(pos2 + 1, pos);
                        } else {
                            loop = false;
                            val = tag.substring(pos2 + 1);
                        }

                        builder.append(ResXmlEncoders.escapeXmlChars(val)).append('"');
                    } else {
                        loop = false;
                    }

                }
            }
        }
        builder.append('>');
    }

    /**
     * Finds index of the string.
     *
     * @param string the string to find
     * @return -1 if the string was not found or the index
     */
    public int find(String string) {
        if (string == null) {
            return -1;
        }
        for (int i = 0; i != m_stringOffsets.length; ++i) {
            int offset = m_stringOffsets[i];
            int length = getShort(m_strings, offset);
            if (length != string.length()) {
                continue;
            }
            int j = 0;
            for (; j != length; ++j) {
                offset += 2;
                if (string.charAt(j) != getShort(m_strings, offset)) {
                    break;
                }
            }
            if (j == length) {
                return i;
            }
        }
        return -1;
    }

    private StringBlock() {
    }

    /**
     * Returns style information - array of int triplets,
     * where in each triplet:
     * * first int is index of tag name ('b','i', etc.)
     * * second int is tag start index in string
     * * third int is tag end index in string
     */
    private int[] getStyle(int index) {
        if (m_styleOffsets == null || m_styles == null || index >= m_styleOffsets.length) {
            return null;
        }
        int offset = m_styleOffsets[index] / 4;
        int style[];
        {
            int count = 0;
            for (int i = offset; i < m_styles.length; ++i) {
                if (m_styles[i] == -1) {
                    break;
                }
                count += 1;
            }
            if (count == 0 || (count % 3) != 0) {
                return null;
            }
            style = new int[count];
        }
        for (int i = offset, j = 0; i < m_styles.length; ) {
            if (m_styles[i] == -1) {
                break;
            }
            style[j++] = m_styles[i++];
        }
        return style;
    }

    private String decodeString(int offset, int length) {
        try {
            return (m_isUTF8 ? UTF8_DECODER : UTF16LE_DECODER).decode(
                    ByteBuffer.wrap(m_strings, offset, length)).toString();
        } catch (CharacterCodingException ex) {
            LOGGER.log(Level.WARNING, null, ex);
            return null;
        }
    }

    private static final int getShort(byte[] array, int offset) {
        return (array[offset + 1] & 0xff) << 8 | array[offset] & 0xff;
    }

    private static final int getShort(int[] array, int offset) {
        int value = array[offset / 4];
        if ((offset % 4) / 2 == 0) {
            return (value & 0xFFFF);
        } else {
            return (value >>> 16);
        }
    }

    private static final int[] getUtf8(byte[] array, int offset) {
        int val = array[offset];
        int length;
        // We skip the utf16 length of the string
        if ((val & 0x80) != 0) {
            offset += 2;
        } else {
            offset += 1;
        }
        // And we read only the utf-8 encoded length of the string
        val = array[offset];
        offset += 1;
        if ((val & 0x80) != 0) {
            int low = (array[offset] & 0xFF);
            length = ((val & 0x7F) << 8) + low;
            offset += 1;
        } else {
            length = val;
        }
        return new int[]{offset, length};
    }

    private static final int[] getUtf16(byte[] array, int offset) {
        int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF);

        if ((val & 0x8000) != 0) {
            int high = (array[offset + 3] & 0xFF) << 8;
            int low = (array[offset + 2] & 0xFF);
            int len_value = ((val & 0x7FFF) << 16) + (high + low);
            return new int[]{4, len_value * 2};

        }
        return new int[]{2, val * 2};
    }

    private int[] m_stringOffsets;
    private byte[] m_strings;
    private int[] m_styleOffsets;
    private int[] m_styles;
    private boolean m_isUTF8;
    private int[] m_stringOwns;

    private final CharsetDecoder UTF16LE_DECODER = Charset.forName("UTF-16LE").newDecoder();
    private final CharsetDecoder UTF8_DECODER = Charset.forName("UTF-8").newDecoder();
    private static final Logger LOGGER = Logger.getLogger(StringBlock.class.getName());

    // ResChunk_header = header.type (0x0001) + header.headerSize (0x001C)
    private static final int CHUNK_STRINGPOOL_TYPE = 0x001C0001;
    private static final int CHUNK_NULL_TYPE = 0x00000000;
    private static final int UTF8_FLAG = 0x00000100;
}