/******************************************************************************
* Copyright (C) 2013  Fabio Zadrozny
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*     Fabio Zadrozny <fabiofz@gmail.com> - initial API and implementation
******************************************************************************/
package org.python.pydev.shared_ui;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.console.IOConsole;
import org.eclipse.ui.console.IOConsoleOutputStream;
import org.python.pydev.shared_core.log.Log;
import org.python.pydev.shared_ui.utils.RunInUiThread;

/**
 * The main use for this class is:
 * 
 * ConsoleColorCache.getDefault().keepConsoleColorsSynched(console);
 * 
 * It should be called when the console is created and whenever a new stream is created (to
 * set the color for that stream -- which must be registered in a map in the themeConsoleStreamToColor 
 * attribute).
 * 
 * @author Fabio
 */
@SuppressWarnings("deprecation")
public class ConsoleColorCache implements IPreferenceChangeListener {

    private static ConsoleColorCache instance;
    private static Object instanceLock = new Object();

    private List<WeakReference<IOConsole>> weakrefs = new ArrayList<WeakReference<IOConsole>>();
    private final Object referencesLock = new Object();

    private ConsoleColorCache() {
        IEclipsePreferences node = new InstanceScope().getNode("org.eclipse.debug.ui");
        node.addPreferenceChangeListener(this);
    }

    public static ConsoleColorCache getDefault() {
        if (instance == null) {
            synchronized (instanceLock) {
                if (instance == null) {
                    instance = new ConsoleColorCache();
                }
            }
        }
        return instance;
    }

    protected Map<RGB, Color> cache = new HashMap<RGB, Color>(4);

    public Color getColor(RGB rgb) {
        Color color = cache.get(rgb);
        if (color == null) {
            color = new Color(Display.getCurrent(), rgb);
            cache.put(rgb, color);
        }
        return color;
    }

    private Color getDebugColor(String key) {
        IEclipsePreferences node = new InstanceScope().getNode("org.eclipse.debug.ui");
        String color = node.get(key, null);
        if (color != null) {
            try {
                return getDefault().getColor(StringConverter.asRGB(color));
            } catch (Exception e) {
                Log.log(e);
            }
        }
        return null;
    }

    /**
     * In this method we'll make sure a console will have its colors properly updated.
     * 
     * @param console This is the console to keep updated. 
     * 
     * The console should have a getAttribute("themeConsoleStreamToColor") which returns a Map<IOConsoleOutputStream, String>
     * where the values may be: 
     * 
     * "console.output" or "console.error"
     */
    public void keepConsoleColorsSynched(IOConsole console) {
        updateConsole(console);
        addRef(console);
    }

    @SuppressWarnings({ "unchecked" })
    private void updateConsole(final IOConsole console) {
        Runnable r = new Runnable() {

            public void run() {
                //Should be: DebugUIPlugin.getPreferenceColor(IDebugPreferenceConstants.CONSOLE_BAKGROUND_COLOR)
                //but we don't want to add that dependency.
                try {
                    Color color = getDebugColor("org.eclipse.debug.ui.consoleBackground");
                    if (color != null) {
                        console.setBackground(color);
                    }
                } catch (Exception e) {
                    Log.log(e);
                }

                try {
                    Map<IOConsoleOutputStream, String> streamToColor = (Map<IOConsoleOutputStream, String>) console
                            .getAttribute("themeConsoleStreamToColor");
                    if (streamToColor != null) {
                        Set<Entry<IOConsoleOutputStream, String>> entrySet = streamToColor.entrySet();
                        for (Entry<IOConsoleOutputStream, String> entry : entrySet) {
                            String value = entry.getValue();
                            if ("console.output".equals(value)) {
                                Color color = getDebugColor("org.eclipse.debug.ui.outColor");
                                if (color != null) {
                                    entry.getKey().setColor(color);
                                }

                            } else if ("console.error".equals(value)) {
                                Color color = getDebugColor("org.eclipse.debug.ui.errorColor");
                                if (color != null) {
                                    entry.getKey().setColor(color);
                                }

                            } else {
                                Log.log("Unrecognized value (expected console.output or console.error):" + value);
                            }
                        }
                    }
                } catch (Exception e) {
                    Log.log(e);
                }
            }
        };
        RunInUiThread.async(r, true);
    }

    private void addRef(IOConsole console) {
        synchronized (referencesLock) {
            //We'll clear the current references and add the new one if it's not there already.
            int size = weakrefs.size();
            for (int i = 0; i < size; i++) {
                WeakReference<IOConsole> ref = weakrefs.get(i);
                Object object = ref.get();
                if (object == console) {
                    return; //already there (nothing to add).
                }
                if (object == null) {
                    weakrefs.remove(i);
                    i--;
                    size--;
                }
            }
            //Add the new reference.
            weakrefs.add(new WeakReference<IOConsole>(console));
        }
    }

    public void preferenceChange(PreferenceChangeEvent event) {
        String key = event.getKey();
        if ("org.eclipse.debug.ui.consoleBackground".equals(key) || "org.eclipse.debug.ui.outColor".equals(key)
                || "org.eclipse.debug.ui.errorColor".equals(key)) {
            synchronized (referencesLock) {
                ArrayList<IOConsole> currentRefs = getCurrentRefs();
                for (IOConsole console : currentRefs) {
                    updateConsole(console);
                }
            }
        }
    }

    private ArrayList<IOConsole> getCurrentRefs() {
        int size = weakrefs.size();
        ArrayList<IOConsole> currentRefs = new ArrayList<IOConsole>(size);
        for (int i = 0; i < size; i++) {
            WeakReference<IOConsole> ref = weakrefs.get(i);
            IOConsole object = ref.get();
            if (object == null) {
                weakrefs.remove(i);
                i--;
                size--;
            } else {
                currentRefs.add(object);
            }
        }
        return currentRefs;
    }

}
