package util;

// Java imports
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import javax.imageio.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;

// General utility methods
public final class Util {

    // This class cannot be instantiated
    private Util() { }



    ///////////////////////////////////////////////////////////////////////////
    //                                 Types                                 //
    ///////////////////////////////////////////////////////////////////////////

    // Event listener interfaces
    public interface OnClose  { void call(WindowEvent        e); }
    public interface OnClose2 { void call(InternalFrameEvent e); }
    public interface OnFocus  { void call(FocusEvent         e); }
    public interface OnKey    { void call(KeyEvent           e); }
    public interface OnMouse  { void call(MouseEvent         e); }
    public interface OnResize { void call(ComponentEvent     e); }

    // Data class for byte-order marks
    private static class BOM {
        byte[]  mark;
        Charset set;
        BOM(byte[] m, Charset s) { mark = m; set = s; }
    }



    ///////////////////////////////////////////////////////////////////////////
    //                               Constants                               //
    ///////////////////////////////////////////////////////////////////////////

    // Text file byte-order marks
    private static final BOM[] BOMS = {
        new BOM(new byte[] { -17, -69, -65 }, StandardCharsets.UTF_8   ),
        new BOM(new byte[] { - 2, - 1      }, StandardCharsets.UTF_16BE),
        new BOM(new byte[] { - 1, - 2      }, StandardCharsets.UTF_16LE),
    };

    // Filesystem state manager for the current .jar (if any)
    private static final FileSystem JARFS;

    // Static initializer
    static {
        FileSystem fs = null;
        try {
            fs = FileSystems.newFileSystem(
                new URI("jar:" + Util.class.getProtectionDomain()
                    .getCodeSource().getLocation().toString()),
                new HashMap<String, String>(),
                Util.class.getClassLoader()
            );
        } catch (Exception e) { }
        JARFS = fs;
    }



    ///////////////////////////////////////////////////////////////////////////
    //                            Static Methods                             //
    ///////////////////////////////////////////////////////////////////////////

    // Apply an alpha value to a Color object
    public static Color alpha(Color color, float alpha) {
        int a = (int) Math.round(alpha * 255);
        return color == null || a < 0 || a > 255 ? null :
            new Color(color.getRGB() & 0x00FFFFFF | a << 24, true);
    }

    // Blend one color into another
    public static Color blend(Color front, Color back, float alpha) {

        // Error checking
        if (front == null || back == null ||
            !Float.isFinite(alpha) || alpha < 0 || alpha > 1)
            return null;

        // Decompose components
        var colors = new Color[] { front, back };
        var argb   = new float[3][4];
        for (int x = 0; x < 2; x++) {
            int bits = colors[x].getRGB();
            for (int y = 3; y >= 0; y++, bits >>= 8)
                argb[x][y] = (bits & 0xFF) / 255.0f;
        }

        // Combine the colors
        argb[2][0] = argb[0][0] + argb[1][0] * (1 - argb[0][0]);
        for (int x = 1; x < 4; x++)
            argb[2][x] = (
                argb[0][x] * argb[0][0] +
                argb[1][x] * argb[1][0] * (1 - argb[0][0])
            ) / argb[2][0];

        // Produce the resulting Color object
        int bits = 0;
        for (int x = 0; x < 4; x++)
            bits = bits << 8 | (int) Math.round(argb[2][x] * 255);
        return new Color(bits, true);
    }

    // Read a file from disk
    public static byte[] fileRead(File file) {
        FileInputStream stream = null;
        byte[]          data   = null;
        try {
            stream = new FileInputStream(file);
            data    = stream.readAllBytes();
        } catch (Exception e) { }
        try { stream.close(); } catch (Exception e) { }
        return data;
    }

    // Read a file, first from disk, then from .jar
    public static byte[] fileRead(String filename) {
        InputStream stream = null;

        // Open the file on disk
        try { stream = new FileInputStream(filename); }

        // File on disk could not be opened, so get resource from .jar
        catch (Exception e) {
            stream = Util.class.getResourceAsStream("/" + filename);
            if (stream == null)
                return null; // Resource in .jar could not be found
        }

        // Read the file data into memory
        byte[] data = null;
        try { data = stream.readAllBytes(); } catch (Exception e) { }
        try { stream.close();               } catch (Exception e) { }
        return data;
    }

    // Read an image file as an icon
    public static Icon iconRead(String filename) {
        BufferedImage img = imageRead(filename);
        return img == null ? null : new ImageIcon(img);
    }

    // Read an image file by filename
    public static BufferedImage imageRead(String filename) {
        try { return
            ImageIO.read(new ByteArrayInputStream(fileRead(filename)));
        } catch (Exception e) { return null; }
    }

    // Read an image file by handle
    public static BufferedImage imageRead(File file) {
        try { return imageRead(file.getAbsolutePath()); }
        catch (Exception e) { return null; }
    }

    // Produce a list of files contained in a directory
    // If the directory is not found on disk, looks for it in the .jar
    public static String[] listFiles(String path) {

        // Check for the directory on disk
        var file = new File(path);
        if (file.exists() && file.isDirectory())
            return file.list();

        // Not executing out of a .jar
        if (JARFS == null)
            return null;

        // Check for the directory in the .jar
        try {
            var list = Files.list(JARFS.getPath("/" + path)).toArray();
            var ret  = new String[list.length];
            for (int x = 0; x < list.length; x++) {
                path = "/" + ((Path) list[x]).toString();
                ret[x] = path.substring(path.lastIndexOf("/") + 1);
            }
            return ret;
        } catch (Exception e) { }

        // The directory was not found
        return null;
    }

    // Produce a WindowListener with a functional interface
    public static WindowListener onClose(OnClose close) {
        return new WindowListener() {
            public void windowActivated  (WindowEvent e) { }
            public void windowClosed     (WindowEvent e) { }
            public void windowDeactivated(WindowEvent e) { }
            public void windowDeiconified(WindowEvent e) { }
            public void windowIconified  (WindowEvent e) { }
            public void windowOpened     (WindowEvent e) { }
            public void windowClosing    (WindowEvent e)
                { if (close != null) close.call(e); }
        };
    }

    // Produce an InternalFrameListener with a functional interface
    public static InternalFrameListener onClose2(OnClose2 close) {
        return new InternalFrameListener() {
            public void internalFrameActivated  (InternalFrameEvent e) { }
            public void internalFrameClosed     (InternalFrameEvent e) { }
            public void internalFrameDeactivated(InternalFrameEvent e) { }
            public void internalFrameDeiconified(InternalFrameEvent e) { }
            public void internalFrameIconified  (InternalFrameEvent e) { }
            public void internalFrameOpened     (InternalFrameEvent e) { }
            public void internalFrameClosing    (InternalFrameEvent e)
                { if (close != null) close.call(e); }
        };
    }

    // Produce a FocusListener with functional interfaces
    public static FocusListener onFocus(OnFocus focus, OnFocus blur) {
        return new FocusListener() {
            public void focusGained(FocusEvent e)
                { if (focus != null) focus.call(e); }
            public void focusLost  (FocusEvent e)
                { if (blur  != null) blur .call(e); }
        };
    }

    // Produce a KeyListener with functional interfaces
    public static KeyListener onKey(OnKey down, OnKey up) {
        return new KeyListener() {
            public void keyTyped   (KeyEvent e) { }
            public void keyPressed (KeyEvent e)
                { if (down != null) down.call(e); }
            public void keyReleased(KeyEvent e)
                { if (up   != null) up  .call(e); }
        };
    }

    // Produce a MouseMotionListener with a functional interface
    public static MouseMotionListener onMouseMove(OnMouse move, OnMouse drag) {
        return new MouseMotionListener() {
            public void mouseMoved  (MouseEvent e)
                { if (move != null) move.call(e); }
            public void mouseDragged(MouseEvent e)
                { if (drag != null) drag.call(e); }
        };
    }

    // Produce a MouseListener with functional interfaces
    public static MouseListener onMouse(OnMouse down, OnMouse up) {
        return new MouseListener() {
            public void mouseClicked (MouseEvent e) { }
            public void mouseEntered (MouseEvent e) { }
            public void mouseExited  (MouseEvent e) { }
            public void mousePressed (MouseEvent e)
                { if (down != null) down.call(e); }
            public void mouseReleased(MouseEvent e)
                { if (up   != null) up  .call(e); }
        };
    }

    // Produce a ComponentListener with a functional interface
    public static ComponentListener onResize(OnResize resize) {
        return new ComponentListener() {
            public void componentHidden (ComponentEvent e) { }
            public void componentMoved  (ComponentEvent e) { }
            public void componentShown  (ComponentEvent e) { }
            public void componentResized(ComponentEvent e)
                { if (resize != null) resize.call(e); }
        };
    }

    // Configure the Swing look-and-feel with the system theme
    public static boolean setSystemLAF() {
        try {
            UIManager.setLookAndFeel(
                UIManager.getSystemLookAndFeelClassName());
            return true;
        } catch (Exception e) { return false; }
    }

    // Produce a JSplitPane with an un-styled divider
    public static JSplitPane splitPane(int orientation) {
        var split = new JSplitPane(orientation, null, null);
        split.setContinuousLayout(true);
        split.setDividerSize(2);
        split.setUI(new BasicSplitPaneUI() {
            public BasicSplitPaneDivider createDefaultDivider() {
                return new BasicSplitPaneDivider(this) {
                    public void setBorder(Border b) { }
                };
            }
        });
        split.setBorder(null);
        return split;
    }

    // Read a text file, accounting for BOMs
    public static String textRead(String filename) {

        // Read the file
        byte[] data = fileRead(filename);
        if (data == null)
            return null;

        // Configure working variables
        byte[]  mark = null;
        Charset set  = StandardCharsets.ISO_8859_1; // Default charset

        // Check for a byte-order mark
        outer: for (BOM bom : BOMS) {
            if (data.length < bom.mark.length)
                continue;
            for (int x = 0; x < bom.mark.length; x++)
                if (data[x] != bom.mark[x])
                    continue outer;
            mark = bom.mark;
            set  = bom.set;
            break;
        }

        // Remove the byte-order mark from the data
        if (mark != null) {
            byte[] temp = new byte[data.length - mark.length];
            System.arraycopy(data, mark.length, temp, 0, temp.length);
            data = temp;
        }

        // Produce a string
        return new String(data, set);
    }

}