/*
 * Copyright (c) 2001, 2023, 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.swing.plaf;

import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Toolkit;
import java.awt.Window;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.FontUIResource;

import sun.awt.AppContext;

/**
 * Wrapper for a value from the desktop. The value is lazily looked up, and
 * can be accessed using the <code>UIManager.ActiveValue</code> method
 * <code>createValue</code>. If the underlying desktop property changes this
 * will force the UIs to update all known Frames. You can invoke
 * <code>invalidate</code> to force the value to be fetched again.
 */
public class DesktopProperty implements UIDefaults.ActiveValue {
    private static final StringBuilder DESKTOP_PROPERTY_UPDATE_PENDING_KEY =
            new StringBuilder("DesktopPropertyUpdatePending");

    /**
     * ReferenceQueue of unreferenced WeakPCLs.
     */
    private static final ReferenceQueue<DesktopProperty> queue = new ReferenceQueue<DesktopProperty>();

    /**
     * PropertyChangeListener attached to the Toolkit.
     */
    private WeakPCL pcl;
    /**
     * Key used to lookup value from desktop.
     */
    private final String key;
    /**
     * Value to return.
     */
    private Object value;
    /**
     * Fallback value in case we get null from desktop.
     */
    private final Object fallback;


    /**
     * Cleans up any lingering state held by unrefeernced
     * DesktopProperties.
     */
    public static void flushUnreferencedProperties() {
        WeakPCL pcl;

        while ((pcl = (WeakPCL)queue.poll()) != null) {
            pcl.dispose();
        }
    }


    /**
     * Sets whether or not an updateUI call is pending.
     */
    private static synchronized void setUpdatePending(boolean update) {
        AppContext.getAppContext()
                .put(DESKTOP_PROPERTY_UPDATE_PENDING_KEY, update);
    }

    /**
     * Returns true if a UI update is pending.
     */
    private static synchronized boolean isUpdatePending() {
        return Boolean.TRUE.equals(AppContext.getAppContext()
                .get(DESKTOP_PROPERTY_UPDATE_PENDING_KEY));
    }

    /**
     * Updates the UIs of all the known Frames.
     */
    protected void updateAllUIs() {
        Frame[] appFrames = Frame.getFrames();
        for (Frame appFrame : appFrames) {
            updateWindowUI(appFrame);
        }
    }

    /**
     * Updates the UI of the passed in window and all its children.
     */
    private static void updateWindowUI(Window window) {
        SwingUtilities.updateComponentTreeUI(window);
        Window[] ownedWins = window.getOwnedWindows();
        for (Window ownedWin : ownedWins) {
            updateWindowUI(ownedWin);
        }
    }


    /**
     * Creates a DesktopProperty.
     *
     * @param key Key used in looking up desktop value.
     * @param fallback Value used if desktop property is null.
     */
    public DesktopProperty(String key, Object fallback) {
        this.key = key;
        this.fallback = fallback;
        // The only sure fire way to clear our references is to create a
        // Thread and wait for a reference to be added to the queue.
        // Because it is so rare that you will actually change the look
        // and feel, this stepped is forgoed and a middle ground of
        // flushing references from the constructor is instead done.
        // The implication is that once one DesktopProperty is created
        // there will most likely be n (number of DesktopProperties created
        // by the LookAndFeel) WeakPCLs around, but this number will not
        // grow past n.
        flushUnreferencedProperties();
    }

    /**
     * UIManager.LazyValue method, returns the value from the desktop
     * or the fallback value if the desktop value is null.
     */
    public Object createValue(UIDefaults table) {
        if (value == null) {
            value = configureValue(getValueFromDesktop());
            if (value == null) {
                value = configureValue(getDefaultValue());
            }
        }
        return value;
    }

    /**
     * Returns the value from the desktop.
     */
    protected Object getValueFromDesktop() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();

        if (pcl == null) {
            pcl = new WeakPCL(this, getKey(), UIManager.getLookAndFeel());
            toolkit.addPropertyChangeListener(getKey(), pcl);
        }

        return toolkit.getDesktopProperty(getKey());
    }

    /**
     * Returns the value to use if the desktop property is null.
     */
    protected Object getDefaultValue() {
        return fallback;
    }

    /**
     * Invalidates the current value.
     *
     * @param laf the LookAndFeel this DesktopProperty was created with
     */
    public void invalidate(LookAndFeel laf) {
        invalidate();
    }

    /**
     * Invalidates the current value so that the next invocation of
     * <code>createValue</code> will ask for the property again.
     */
    public void invalidate() {
        value = null;
    }

    /**
     * Requests that all components in the GUI hierarchy be updated
     * to reflect dynamic changes in this {@literal look&feel}. This update occurs
     * by uninstalling and re-installing the UI objects. Requests are
     * batched and collapsed into a single update pass because often
     * many desktop properties will change at once.
     */
    protected void updateUI() {
        if (!isUpdatePending()) {
            setUpdatePending(true);
            Runnable uiUpdater = new Runnable() {
                public void run() {
                    updateAllUIs();
                    setUpdatePending(false);
                }
            };
            SwingUtilities.invokeLater(uiUpdater);
        }
    }

    /**
     * Configures the value as appropriate for a defaults property in
     * the UIDefaults table.
     */
    protected Object configureValue(Object value) {
        if (value != null) {
            if (value instanceof Color) {
                return new ColorUIResource((Color)value);
            }
            else if (value instanceof Font) {
                return new FontUIResource((Font)value);
            }
            else if (value instanceof UIDefaults.LazyValue) {
                value = ((UIDefaults.LazyValue)value).createValue(null);
            }
            else if (value instanceof UIDefaults.ActiveValue) {
                value = ((UIDefaults.ActiveValue)value).createValue(null);
            }
        }
        return value;
    }

    /**
     * Returns the key used to lookup the desktop properties value.
     */
    protected String getKey() {
        return key;
    }

    /**
     * As there is typically only one Toolkit, the PropertyChangeListener
     * is handled via a WeakReference so as not to pin down the
     * DesktopProperty.
     */
    private static class WeakPCL extends WeakReference<DesktopProperty>
                               implements PropertyChangeListener {
        private String key;
        private LookAndFeel laf;

        WeakPCL(DesktopProperty target, String key, LookAndFeel laf) {
            super(target, queue);
            this.key = key;
            this.laf = laf;
        }

        public void propertyChange(PropertyChangeEvent pce) {
            DesktopProperty property = get();

            if (property == null || laf != UIManager.getLookAndFeel()) {
                // The property was GC'ed, we're no longer interested in
                // PropertyChanges, remove the listener.
                dispose();
            }
            else {
                property.invalidate(laf);
                property.updateUI();
            }
        }

        void dispose() {
            Toolkit.getDefaultToolkit().removePropertyChangeListener(key, this);
        }
    }
}
