/*
 * Copyright (c) 2011, 2017, 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 apple.laf;

import java.nio.*;
import java.util.*;

import apple.laf.JRSUIConstants.*;

public final class JRSUIControl {
    private static native int initNativeJRSUI();

    private static native long getPtrOfBuffer(ByteBuffer byteBuffer);
    private static native long getCFDictionary(boolean flipped);
    private static native void disposeCFDictionary(long cfDictionaryPtr);

    private static native int syncChanges(long cfDictionaryPtr, long byteBufferPtr);

//    private static native int paint(long cfDictionaryPtr, long oldProperties, long newProperties, OSXSurfaceData osxsd, double x, double y, double w, double h);
//    private static native int paintChanges(long cfDictionaryPtr, long byteBufferPtr, long oldProperties, long newProperties, OSXSurfaceData osxsd, double x, double y, double w, double h);

    private static native int paintToCGContext                    (long cgContext,    long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h);
    private static native int paintChangesToCGContext            (long cgContext,    long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h, long byteBufferPtr);

    private static native int paintImage        (int[] data, int imgW, int imgH,    long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h);
    private static native int paintChangesImage    (int[] data, int imgW, int imgH,    long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h, long byteBufferPtr);

    private static native int getNativeHitPart(                            long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h, double hitX, double hitY);
    private static native void getNativePartBounds(final double[] rect,    long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h, int part);
    private static native double getNativeScrollBarOffsetChange(        long cfDictionaryPtr, long oldProperties, long newProperties, double x, double y, double w, double h, int offset, int visibleAmount, int extent);

    private static final int INCOHERENT = 2;
    private static final int NOT_INIT = 1;
    private static final int SUCCESS = 0;
    private static final int NULL_PTR = -1;
    private static final int NULL_CG_REF = -2;

    private static int nativeJRSInitialized = NOT_INIT;


    public static void initJRSUI() {
        if (nativeJRSInitialized == SUCCESS) return;
        nativeJRSInitialized = initNativeJRSUI();
        if (nativeJRSInitialized != SUCCESS) throw new RuntimeException("JRSUI could not be initialized (" + nativeJRSInitialized + ").");
    }

    private static final int NIO_BUFFER_SIZE = 128;
    private static class ThreadLocalByteBuffer {
        final ByteBuffer buffer;
        final long ptr;

        public ThreadLocalByteBuffer() {
            buffer = ByteBuffer.allocateDirect(NIO_BUFFER_SIZE);
            buffer.order(ByteOrder.nativeOrder());
            ptr = getPtrOfBuffer(buffer);
        }
    }

    private static final ThreadLocal<ThreadLocalByteBuffer> threadLocal = new ThreadLocal<ThreadLocalByteBuffer>();
    private static ThreadLocalByteBuffer getThreadLocalBuffer() {
        ThreadLocalByteBuffer byteBuffer = threadLocal.get();
        if (byteBuffer != null) return byteBuffer;

        byteBuffer = new ThreadLocalByteBuffer();
        threadLocal.set(byteBuffer);
        return byteBuffer;
    }

    private final HashMap<Key, DoubleValue> nativeMap;
    private final HashMap<Key, DoubleValue> changes;
    private long cfDictionaryPtr;

    private long priorEncodedProperties;
    private long currentEncodedProperties;
    private final boolean flipped;

    public JRSUIControl(final boolean flipped){
        this.flipped = flipped;
        cfDictionaryPtr = getCFDictionary(flipped);
        if (cfDictionaryPtr == 0) throw new RuntimeException("Unable to create native representation");
        nativeMap = new HashMap<Key, DoubleValue>();
        changes = new HashMap<Key, DoubleValue>();
    }

    JRSUIControl(final JRSUIControl other) {
        flipped = other.flipped;
        cfDictionaryPtr = getCFDictionary(flipped);
        if (cfDictionaryPtr == 0) throw new RuntimeException("Unable to create native representation");
        nativeMap = new HashMap<Key, DoubleValue>();
        changes = new HashMap<Key, DoubleValue>(other.nativeMap);
        changes.putAll(other.changes);
    }

    @SuppressWarnings("deprecation")
    protected synchronized void finalize() throws Throwable {
        if (cfDictionaryPtr == 0) return;
        disposeCFDictionary(cfDictionaryPtr);
        cfDictionaryPtr = 0;
    }


    enum BufferState {
        NO_CHANGE,
        ALL_CHANGES_IN_BUFFER,
        SOME_CHANGES_IN_BUFFER,
        CHANGE_WONT_FIT_IN_BUFFER;
    }

    private BufferState loadBufferWithChanges(final ThreadLocalByteBuffer localByteBuffer) {
        final ByteBuffer buffer = localByteBuffer.buffer;
        buffer.rewind();

        for (final JRSUIConstants.Key key : new HashSet<JRSUIConstants.Key>(changes.keySet())) {
            final int changeIndex = buffer.position();
            final JRSUIConstants.DoubleValue value = changes.get(key);

            try {
                buffer.putLong(key.getConstantPtr());
                buffer.put(value.getTypeCode());
                value.putValueInBuffer(buffer);
            } catch (final BufferOverflowException e) {
                return handleBufferOverflow(buffer, changeIndex);
            } catch (final RuntimeException e) {
                System.err.println(this);
                throw e;
            }

            if (buffer.position() >= NIO_BUFFER_SIZE - 8) {
                return handleBufferOverflow(buffer, changeIndex);
            }

            changes.remove(key);
            nativeMap.put(key, value);
        }

        buffer.putLong(0);
        return BufferState.ALL_CHANGES_IN_BUFFER;
    }

    private BufferState handleBufferOverflow(final ByteBuffer buffer, final int changeIndex) {
        if (changeIndex == 0) {
            buffer.putLong(0, 0);
            return BufferState.CHANGE_WONT_FIT_IN_BUFFER;
        }

        buffer.putLong(changeIndex, 0);
        return BufferState.SOME_CHANGES_IN_BUFFER;
    }

    private synchronized void set(final JRSUIConstants.Key key, final JRSUIConstants.DoubleValue value) {
        final JRSUIConstants.DoubleValue existingValue = nativeMap.get(key);

        if (existingValue != null && existingValue.equals(value)) {
            changes.remove(key);
            return;
        }

        changes.put(key, value);
    }

    public void set(final JRSUIState state) {
        state.apply(this);
    }

    void setEncodedState(final long state) {
        currentEncodedProperties = state;
    }

    void set(final JRSUIConstants.Key key, final double value) {
        set(key, new JRSUIConstants.DoubleValue(value));
    }

//    private static final Color blue = new Color(0x00, 0x00, 0xFF, 0x40);
//    private static void paintDebug(Graphics2D g, double x, double y, double w, double h) {
//        final Color prev = g.getColor();
//        g.setColor(blue);
//        g.drawRect((int)x, (int)y, (int)w, (int)h);
//        g.setColor(prev);
//    }

//    private static int paintsWithNoChange = 0;
//    private static int paintsWithChangesThatFit = 0;
//    private static int paintsWithChangesThatOverflowed = 0;

    public void paint(final int[] data, final int imgW, final int imgH, final double x, final double y, final double w, final double h) {
        paintImage(data, imgW, imgH, x, y, w, h);
        priorEncodedProperties = currentEncodedProperties;
    }

    private synchronized int paintImage(final int[] data, final int imgW, final int imgH, final double x, final double y, final double w, final double h) {
        if (changes.isEmpty()) {
//            paintsWithNoChange++;
            return paintImage(data, imgW, imgH, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h);
        }

        final ThreadLocalByteBuffer localByteBuffer = getThreadLocalBuffer();
        BufferState bufferState = loadBufferWithChanges(localByteBuffer);

        // fast tracking this, since it's the likely scenario
        if (bufferState == BufferState.ALL_CHANGES_IN_BUFFER) {
//            paintsWithChangesThatFit++;
            return paintChangesImage(data, imgW, imgH, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, localByteBuffer.ptr);
        }

        while (bufferState == BufferState.SOME_CHANGES_IN_BUFFER) {
            final int status = syncChanges(cfDictionaryPtr, localByteBuffer.ptr);
            if (status != SUCCESS) throw new RuntimeException("JRSUI failed to sync changes into the native buffer: " + this);
            bufferState = loadBufferWithChanges(localByteBuffer);
        }

        if (bufferState == BufferState.CHANGE_WONT_FIT_IN_BUFFER) {
            throw new RuntimeException("JRSUI failed to sync changes to the native buffer, because some change was too big: " + this);
        }

        // implicitly ALL_CHANGES_IN_BUFFER, now that we sync'd the buffer down to native a few times
//        paintsWithChangesThatOverflowed++;
        return paintChangesImage(data, imgW, imgH, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, localByteBuffer.ptr);
    }

    public void paint(final long cgContext, final double x, final double y, final double w, final double h) {
        paintToCGContext(cgContext, x, y, w, h);
        priorEncodedProperties = currentEncodedProperties;
    }

    private synchronized int paintToCGContext(final long cgContext, final double x, final double y, final double w, final double h) {
        if (changes.isEmpty()) {
//            paintsWithNoChange++;
            return paintToCGContext(cgContext, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h);
        }

        final ThreadLocalByteBuffer localByteBuffer = getThreadLocalBuffer();
        BufferState bufferState = loadBufferWithChanges(localByteBuffer);

        // fast tracking this, since it's the likely scenario
        if (bufferState == BufferState.ALL_CHANGES_IN_BUFFER) {
//            paintsWithChangesThatFit++;
            return paintChangesToCGContext(cgContext, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, localByteBuffer.ptr);
        }

        while (bufferState == BufferState.SOME_CHANGES_IN_BUFFER) {
            final int status = syncChanges(cfDictionaryPtr, localByteBuffer.ptr);
            if (status != SUCCESS) throw new RuntimeException("JRSUI failed to sync changes into the native buffer: " + this);
            bufferState = loadBufferWithChanges(localByteBuffer);
        }

        if (bufferState == BufferState.CHANGE_WONT_FIT_IN_BUFFER) {
            throw new RuntimeException("JRSUI failed to sync changes to the native buffer, because some change was too big: " + this);
        }

        // implicitly ALL_CHANGES_IN_BUFFER, now that we sync'd the buffer down to native a few times
//        paintsWithChangesThatOverflowed++;
        return paintChangesToCGContext(cgContext, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, localByteBuffer.ptr);
    }


    Hit getHitForPoint(final int x, final int y, final int w, final int h, final int hitX, final int hitY) {
        sync();
        // reflect hitY about the midline of the control before sending to native
        final Hit hit = JRSUIConstants.getHit(getNativeHitPart(cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, hitX, 2 * y + h - hitY));
        priorEncodedProperties = currentEncodedProperties;
        return hit;
    }

    void getPartBounds(final double[] rect, final int x, final int y, final int w, final int h, final int part) {
        if (rect == null) throw new NullPointerException("Cannot load null rect");
        if (rect.length != 4) throw new IllegalArgumentException("Rect must have four elements");

        sync();
        getNativePartBounds(rect, cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, part);
        priorEncodedProperties = currentEncodedProperties;
    }

    double getScrollBarOffsetChange(final int x, final int y, final int w, final int h, final int offset, final int visibleAmount, final int extent) {
        sync();
        final double offsetChange = getNativeScrollBarOffsetChange(cfDictionaryPtr, priorEncodedProperties, currentEncodedProperties, x, y, w, h, offset, visibleAmount, extent);
        priorEncodedProperties = currentEncodedProperties;
        return offsetChange;
    }

    private void sync() {
        if (changes.isEmpty()) return;

        final ThreadLocalByteBuffer localByteBuffer = getThreadLocalBuffer();
        BufferState bufferState = loadBufferWithChanges(localByteBuffer);
        if (bufferState == BufferState.ALL_CHANGES_IN_BUFFER) {
            final int status = syncChanges(cfDictionaryPtr, localByteBuffer.ptr);
            if (status != SUCCESS) throw new RuntimeException("JRSUI failed to sync changes into the native buffer: " + this);
            return;
        }

        while (bufferState == BufferState.SOME_CHANGES_IN_BUFFER) {
            final int status = syncChanges(cfDictionaryPtr, localByteBuffer.ptr);
            if (status != SUCCESS) throw new RuntimeException("JRSUI failed to sync changes into the native buffer: " + this);
            bufferState = loadBufferWithChanges(localByteBuffer);
        }

        if (bufferState == BufferState.CHANGE_WONT_FIT_IN_BUFFER) {
            throw new RuntimeException("JRSUI failed to sync changes to the native buffer, because some change was too big: " + this);
        }
    }

    @Override
    public int hashCode() {
        int bits = (int)(currentEncodedProperties ^ (currentEncodedProperties >>> 32));
        bits ^= nativeMap.hashCode();
        bits ^= changes.hashCode();
        return bits;
    }

    @Override
    public boolean equals(final Object obj) {
        if (!(obj instanceof JRSUIControl)) return false;
        final JRSUIControl other = (JRSUIControl)obj;
        if (currentEncodedProperties != other.currentEncodedProperties) return false;
        if (!nativeMap.equals(other.nativeMap)) return false;
        if (!changes.equals(other.changes)) return false;
        return true;
    }

    @Override
    public String toString() {
        final StringBuilder builder = new StringBuilder("JRSUIControl[inNative:");
        builder.append(Arrays.toString(nativeMap.entrySet().toArray()));
        builder.append(", changes:");
        builder.append(Arrays.toString(changes.entrySet().toArray()));
        builder.append("]");
        return builder.toString();
    }
}