/*
 * Copyright (c) 2010, 2011, Oracle  and/or its affiliates. All rights reserved.
 */

package com.sun.javafx.tk.quantum;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.scene.image.Image;
import javafx.scene.input.DataFormat;
import javafx.scene.input.TransferMode;
import javafx.util.Pair;

import com.sun.glass.ui.Clipboard;
import com.sun.glass.ui.ClipboardAssistance;
import com.sun.glass.ui.Pixels;
import com.sun.javafx.tk.ImageLoader;
import com.sun.javafx.tk.TKClipboard;
import com.sun.javafx.tk.Toolkit;


/**
 * The implementation of TKClipboard, which is used both for clipboards
 * and dragboards.
 * 
 * @author Morris Meyer
 */
public class QuantumClipboard implements TKClipboard {

    /**
     * Handle to the Glass peer with which we work. Not sure what the good
     * of this class is. Need to check with Glass guys (I think it can be toasted now).
     */
    private ClipboardAssistance systemAssistant;

    /**
     * Disallow direct creation of QuantumClipboard
     */
    private QuantumClipboard() { }

    /**
     * Distinguishes between clipboard and dragboard. This is needed
     * because dragboard's flush() starts DnD operation so it mustn't be
     * called too early.
     */
    private boolean isCaching;

    /**
     * Cache of the data used for dragboard between setting them and flushing
     * them to system dragboard.
     */
    private List<Pair<DataFormat, Object>> dataCache;

    /**
     * Cache of the transfer modes used for dragboard between setting them and
     * flushing them to system dragboard.
     */
    private Set<TransferMode> transferModesCache;

    /**
     * Gets an instance of QuantumClipboard for the given assistant. This may be
     * a new instance after each call.
     * @param assistant
     * @return
     */
    public static QuantumClipboard getClipboardInstance(ClipboardAssistance assistant) {
        QuantumClipboard c = new QuantumClipboard();
        c.systemAssistant = assistant;
        c.isCaching = false;
        return c;
    }

    /**
     * Gets an instance of QuantumClipboard for the given assistant for usage
     * as dragboard during drag and drop. It doesn't flush the data implicitly
     * and caches them until flush() is called. This may be
     * a new instance after each call.
     * @param assistant
     * @return
     */
    public static QuantumClipboard getDragboardInstance(ClipboardAssistance assistant) {
        QuantumClipboard c = new QuantumClipboard();
        c.systemAssistant = assistant;
        c.isCaching = true;
        return c;
    }
    
    public static int transferModesToClipboardActions(final Set<TransferMode> tms) {
        int actions = Clipboard.ACTION_NONE;
        for (TransferMode t : tms) {
            switch (t) {
                case COPY:
                    actions |= Clipboard.ACTION_COPY;
                    break;
                case MOVE:
                    actions |= Clipboard.ACTION_MOVE;
                    break;
                case LINK:
                    actions |= Clipboard.ACTION_REFERENCE;
                    break;
                default:
                    throw new IllegalArgumentException(
                            "unsupported TransferMode " + tms);
            }
        }
        return actions;
    }

    public void setSupportedTransferMode(Set<TransferMode> tm) {

        if (isCaching) {
            transferModesCache = tm;
        }

        final int actions = transferModesToClipboardActions(tm);

        systemAssistant.setSupportedActions(actions);
    }
    
    public static Set<TransferMode> clipboardActionsToTransferModes(final int actions) {
        final Set<TransferMode> tms = EnumSet.noneOf(TransferMode.class);

        if ((actions & Clipboard.ACTION_COPY) != 0) {
            tms.add(TransferMode.COPY);
        }
        if ((actions & Clipboard.ACTION_MOVE) != 0) {
            tms.add(TransferMode.MOVE);
        }
        if ((actions & Clipboard.ACTION_REFERENCE) != 0) {
            tms.add(TransferMode.LINK);
        }

        return tms;
    }

    @Override public Set<TransferMode> getTransferModes() {

        if (transferModesCache != null) {
            return EnumSet.copyOf(transferModesCache);
        }
        
        final Set<TransferMode> tms = clipboardActionsToTransferModes(systemAssistant.getSupportedSourceActions());

        return tms;
    }

    /**
     * TODO I have no idea why this is necessary
     */
    public void close() {
        systemAssistant.close();
    }

    /**
     * TODO I have no idea why this has to be public
     */
    public void flush() {
        clearCache();
        systemAssistant.flush();
    }

    @Override public Object getContent(DataFormat dataFormat) {
        if (dataCache != null) {
            for (Pair<DataFormat, Object> pair : dataCache) {
                if (pair.getKey() == dataFormat) {
                    return pair.getValue();
                }
            }
            return null;
        }

        if (dataFormat == DataFormat.IMAGE) {
            return readImage();
        } else if (dataFormat == DataFormat.URL) {
            // TODO This just isn't right :-(
            return systemAssistant.getData(Clipboard.URI_TYPE); 
        } else if (dataFormat == DataFormat.FILES) {
            Object data = systemAssistant.getData(Clipboard.FILE_LIST_TYPE);
            if (data == null) return Collections.emptyList();
            String[] paths = (String[]) data;
            List<File> list = new ArrayList<File>(paths.length);
            for (int i=0; i<paths.length; i++) {
                list.add(new File(paths[i]));
            }
            return list;
        }

        for (String mimeType : dataFormat.getIdentifiers()) {
            Object data = systemAssistant.getData(mimeType);
            if (data instanceof ByteBuffer) {
                try {
                    ByteBuffer bb = (ByteBuffer) data;
                    ByteArrayInputStream bis = new ByteArrayInputStream(
                            bb.array());
                    ObjectInput in = new ObjectInputStream(bis);
                    data = in.readObject();
                } catch (IOException e) {
                    // ignore, just return the ByteBuffer if we cannot parse it
                } catch (ClassNotFoundException e) {
                    // ignore, just return the ByteBuffer if we cannot parse it
                }
            }
            if (data != null) return data;
        }
        return null;
    }

    private Image readImage() {
        Object rawData = systemAssistant.getData(Clipboard.RAW_IMAGE_TYPE);
        if (rawData == null) {
            Object htmlData = systemAssistant.getData(Clipboard.HTML_TYPE);
            if (htmlData != null) {
                String url = parseIMG(htmlData);
                if (url != null) {
                    return (new Image(url));
                }
            }
            return null;
        } else if (rawData instanceof Image) {
            return (Image) rawData;
        } else {
            com.sun.prism.Image platformImage = PixelUtils.pixelsToImage((Pixels)rawData);
            ImageLoader il =  Toolkit.getToolkit().loadPlatformImage(platformImage);
            return Image.impl_fromPlatformImage(il);
        }
    }

    private static final Pattern findTagIMG =
            Pattern.compile("IMG\\s+SRC=\\\"([^\\\"]+)\\\"",
                            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

    private String parseIMG(Object data) {
        if (data == null) {
            return null;
        }
        if ((data instanceof String) == false) {
            return null;
        }
        String str = (String)data;
        Matcher matcher = findTagIMG.matcher(str);
        if (matcher.find()) {
             return (matcher.group(1));
        } else {
            return null;
        }
    }

    private boolean placeImage(final Image image) {
        if (image == null) {
            return false;
        }

        String url = image.impl_getUrl();
        if (url == null || PixelUtils.supportedFormatType(url)) {
            com.sun.prism.Image prismImage =
                        (com.sun.prism.Image)image.impl_getPlatformImage();
            Pixels pixels = PixelUtils.imageToPixels(prismImage);
            if (pixels != null) {
                systemAssistant.setData(Clipboard.RAW_IMAGE_TYPE, pixels);
                return true;
            } else {
                return false;
            }
        } else {
            systemAssistant.setData(Clipboard.URI_TYPE, url);
            return true;
        }
    }


//    @Override public void clear() {
//        systemAssistant.emptyCache();
//    }

    @Override public Set<DataFormat> getContentTypes() {

        Set<DataFormat> set = new HashSet<DataFormat>();

        if (dataCache != null) {
            for (Pair<DataFormat, Object> pair : dataCache) {
                set.add(pair.getKey());
            }
            return set;
        }

        String[] types = systemAssistant.getMimeTypes();
        if (types == null) {
            return set;
        }
        for (String t: types) {
            if (t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) {
                set.add(DataFormat.IMAGE);
            } else if (t.equalsIgnoreCase(Clipboard.URI_TYPE)) {
                set.add(DataFormat.URL);
            } else if (t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) {
                set.add(DataFormat.FILES);
            } else if (t.equalsIgnoreCase(Clipboard.HTML_TYPE)) {
                set.add(DataFormat.HTML);
                // RT-16812 - IE puts images on the clipboard in a HTML IMG url
                if (parseIMG(systemAssistant.getData(Clipboard.HTML_TYPE)) != null) {
                    set.add(DataFormat.IMAGE);
                }
            } else {
                DataFormat dataFormat = DataFormat.lookupMimeType(t);
                if (dataFormat != null) {
                    set.add(dataFormat);
                }
            }
        }
        return set;
    }
        
    @Override public boolean hasContent(DataFormat dataFormat) {
        if (dataCache != null) {
            for (Pair<DataFormat, Object> pair : dataCache) {
                if (pair.getKey() == dataFormat) {
                    return true;
                }
            }
            return false;
        }

        String[] stypes = systemAssistant.getMimeTypes();
        if (stypes == null) {
            return false;
        }
        for (String t: stypes) {
            if (dataFormat == DataFormat.IMAGE &&
                    t.equalsIgnoreCase(Clipboard.RAW_IMAGE_TYPE)) {
                return true;
            } else if (dataFormat == DataFormat.URL &&
                       t.equalsIgnoreCase(Clipboard.URI_TYPE)) {
                return true;
            } else if (dataFormat == DataFormat.IMAGE &&
                       t.equalsIgnoreCase(Clipboard.HTML_TYPE)) {
                return (parseIMG(systemAssistant.getData(Clipboard.HTML_TYPE)) != null);
            } else if (dataFormat == DataFormat.FILES &&
                    t.equalsIgnoreCase(Clipboard.FILE_LIST_TYPE)) {
                return true;
            }

            DataFormat found = DataFormat.lookupMimeType(t);
            if (found != null && found.equals(dataFormat)) {
                return true;
            }
        }
        return false;
    }
    
    @Override public boolean putContent(Pair<DataFormat, Object>... content) {
        systemAssistant.emptyCache();

        if (isCaching) {
            dataCache = new ArrayList<Pair<DataFormat, Object>>(content.length);
        }

        boolean dataSet = false;
        
        for (Pair<DataFormat, Object> pair : content) {
            final DataFormat format = pair.getKey();
            final Object     data   = pair.getValue();
            
            if (format == null) {
                throw new NullPointerException("Clipboard.putContent: null data format");
            }
            if (data == null) {
                throw new NullPointerException("Clipboard.putContent: null data");
            }
        }

        // For each pair, we need to extract the DataFormat and data associated with
        // that pair. We then will send the data down to Glass, having done
        // some work for well known types (such as Image and File) in order to
        // adapt from FX to Glass requirements. For everything else, we just
        // pass down the mime type and data, for each mime type supported by
        // the DataFormat type.
        for (Pair<DataFormat, Object> pair : content) {
            final DataFormat dataFormat = pair.getKey();
            Object data = pair.getValue();

            if (isCaching) {
                dataCache.add(pair);
            }

            // Images are handled specially. On Windows, the image type supported
            // on the clipboard is a DIB (device independent bitmap), while on Mac
            // it is a TIFF. Other native apps expect this entry on the clipboard
            // for image copy/paste. However other Java apps or FX apps (for example)
            // might expect the JPG bits directly, rather than the DIB / TIFF bits.
            // So what we do is, any IMAGE type DataFormat that comes in will be stored
            // in DIB / TIFF, while specific bits will also be stored (in the future).
            // I don't actually store the real bits yet.
            if (dataFormat == DataFormat.IMAGE && data instanceof Image) {
                placeImage((Image) data);
                dataSet = true;
            } else if (dataFormat == DataFormat.URL) {
                // TODO Weird, but this is how Glass wants it...
                systemAssistant.setData(Clipboard.URI_TYPE, data);
                dataSet = true;
            } else if (dataFormat == DataFormat.RTF) {
                systemAssistant.setData(Clipboard.RTF_TYPE, data);
                dataSet = true;
            } else if (dataFormat == DataFormat.FILES) {
                // Have to convert from List<File> to String[]
                List<File> list = (List<File>)data;
                String[] paths = new String[list.size()];
                int i = 0;
                for (File f : list) {
                    paths[i++] = f.getAbsolutePath();
                }
                systemAssistant.setData(Clipboard.FILE_LIST_TYPE, paths);
                dataSet = true;
            } else {
                if (data instanceof Serializable) {
                    if ((dataFormat != DataFormat.PLAIN_TEXT &&
                            dataFormat != DataFormat.HTML) ||
                            ! (data instanceof String)) {
                        try {
                            ByteArrayOutputStream bos = new ByteArrayOutputStream();
                            ObjectOutput out = new ObjectOutputStream(bos);
                            out.writeObject(data);
                            out.close();
                            data = ByteBuffer.wrap(bos.toByteArray());
                        } catch (IOException e) {
                            throw new IllegalArgumentException("Could not "
                                    + "serialize the data", e);
                        }
                    }
                } else if (!(data instanceof ByteBuffer)) {
                    throw new IllegalArgumentException("Only serializable "
                            + "objects or ByteBuffer can be used as data "
                            + "with data format " + dataFormat);
                }
                for (String mimeType : dataFormat.getIdentifiers()) {
                    systemAssistant.setData(mimeType, data);
                    dataSet = true;
                }
            }
        }
        if (!isCaching) {
            systemAssistant.flush();
        }
        return dataSet;
    }

    private void clearCache() {
        dataCache = null;
        transferModesCache = null;
    }
}
