/*
 * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.awt;

import java.awt.Color;
import java.util.HashMap;
import java.util.Map;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Per-screen XSETTINGS.
 */
public class XSettings {

    /**
     */
    private long serial = -1;


    /**
     * Update these settings with {@code data} obtained from
     * XSETTINGS manager.
     *
     * @param data settings data obtained from
     *     {@code _XSETTINGS_SETTINGS} window property of the
     *     settings manager.
     * @return a {@code Map} of changed settings.
     */
    public Map<String, Object> update(byte[] data) {
        return (new Update(data)).update();
    }


    /**
     * TBS ...
     */
    final class Update {

        /* byte order mark */
        private static final int LITTLE_ENDIAN = 0;
        private static final int BIG_ENDIAN    = 1;

        /* setting type */
        private static final int TYPE_INTEGER = 0;
        private static final int TYPE_STRING  = 1;
        private static final int TYPE_COLOR   = 2;

        private byte[] data;
        private int dlen;
        private int idx;
        private boolean isLittle;
        private long serial = -1;
        private int nsettings = 0;
        private boolean isValid;

        private HashMap<String, Object> updatedSettings;


        /**
         * Construct an Update object for the data read from
         * {@code _XSETTINGS_SETTINGS} property of the XSETTINGS
         * selection owner.
         *
         * @param data {@code _XSETTINGS_SETTINGS} contents.
         */
        Update(byte[] data) {
            this.data = data;

            dlen = data.length;
            if (dlen < 12) {
                // XXX: debug trace?
                return;
            }

            // first byte gives endianness of the data
            // next 3 bytes are unused (pad to 32 bit)
            idx = 0;
            isLittle = (getCARD8() == LITTLE_ENDIAN);

            idx = 4;
            serial = getCARD32();

            // N_SETTINGS is actually CARD32 (i.e. unsigned), but
            // since java doesn't have an unsigned int type, and
            // N_SETTINGS cannot realistically exceed 2^31 (so we
            // gonna use int anyway), just read it as INT32.
            idx = 8;
            nsettings = getINT32();

            updatedSettings = new HashMap<>();

            isValid = true;
        }


        private void needBytes(int n)
            throws IndexOutOfBoundsException
        {
            if (idx + n <= dlen) {
                return;
            }

            throw new IndexOutOfBoundsException("at " + idx
                                                + " need " + n
                                                + " length " + dlen);
        }


        private int getCARD8()
            throws IndexOutOfBoundsException
        {
            needBytes(1);

            int val = data[idx] & 0xff;

            ++idx;
            return val;
        }


        private int getCARD16()
            throws IndexOutOfBoundsException
        {
            needBytes(2);

            int val;
            if (isLittle) {
                val = ((data[idx + 0] & 0xff)      )
                    | ((data[idx + 1] & 0xff) <<  8);
            } else {
                val = ((data[idx + 0] & 0xff) <<  8)
                    | ((data[idx + 1] & 0xff)      );
            }

            idx += 2;
            return val;
        }


        private int getINT32()
            throws IndexOutOfBoundsException
        {
            needBytes(4);

            int val;
            if (isLittle) {
                val = ((data[idx + 0] & 0xff)      )
                    | ((data[idx + 1] & 0xff) <<  8)
                    | ((data[idx + 2] & 0xff) << 16)
                    | ((data[idx + 3] & 0xff) << 24);
            } else {
                val = ((data[idx + 0] & 0xff) << 24)
                    | ((data[idx + 1] & 0xff) << 16)
                    | ((data[idx + 2] & 0xff) <<  8)
                    | ((data[idx + 3] & 0xff) <<  0);
            }

            idx += 4;
            return val;
        }


        private long getCARD32()
            throws IndexOutOfBoundsException
        {
            return getINT32() & 0x00000000ffffffffL;
        }


        private String getString(int len)
            throws IndexOutOfBoundsException
        {
            needBytes(len);

            String str = new String(data, idx, len, UTF_8);

            idx = (idx + len + 3) & ~0x3;
            return str;
        }


        /**
         * Update settings.
         */
        public Map<String, Object> update() {
            if (!isValid) {
                return null;
            }

            synchronized (XSettings.this) {
                long currentSerial = XSettings.this.serial;

                if (this.serial <= currentSerial) {
                    return null;
                }

                for (int i = 0; i < nsettings && idx < dlen; ++i) {
                    updateOne(currentSerial);
                }

                XSettings.this.serial = this.serial;
            }

            return updatedSettings;
        }


        /**
         * Parses a particular x setting.
         *
         * @throws IndexOutOfBoundsException if there isn't enough
         *     data for a setting.
         */
        private void updateOne(long currentSerial)
            throws IndexOutOfBoundsException,
                   IllegalArgumentException
        {
            int type = getCARD8();
            ++idx;              // pad to next CARD16

            // save position of the property name, skip to serial
            int nameLen = getCARD16();
            int nameIdx = idx;

            // check if we should bother
            idx = (idx + nameLen + 3) & ~0x3; // pad to 32 bit
            long lastChanged = getCARD32();

            // Avoid constructing garbage for properties that has not
            // changed, skip the data for this property.
            if (lastChanged <= currentSerial) { // skip
                if (type == TYPE_INTEGER) {
                    idx += 4;
                } else if (type == TYPE_STRING) {
                    int len = getINT32();
                    idx = (idx + len + 3) & ~0x3;
                } else if (type == TYPE_COLOR) {
                    idx += 8;   // 4 CARD16
                } else {
                    throw new IllegalArgumentException("Unknown type: "
                                                       + type);
                }

                return;
            }

            idx = nameIdx;
            String name = getString(nameLen);
            idx += 4;           // skip serial, parsed above

            Object value = null;
            if (type == TYPE_INTEGER) {
                value = Integer.valueOf(getINT32());
            }
            else if (type == TYPE_STRING) {
                value = getString(getINT32());
            }
            else if (type == TYPE_COLOR) {
                int r = getCARD16();
                int g = getCARD16();
                int b = getCARD16();
                int a = getCARD16();

                value = new Color(r / 65535.0f,
                                  g / 65535.0f,
                                  b / 65535.0f,
                                  a / 65535.0f);
            }
            else {
                throw new IllegalArgumentException("Unknown type: " + type);
            }

            if (name == null) {
                // dtrace???
                return;
            }

            updatedSettings.put(name, value);
        }

    } // class XSettings.Update
}
