package be.msyben.vinylManager.tools;

import be.msyben.vinylManager.exceptions.VinylManagerException;
import be.msyben.vinylManager.properties.Language;
import be.msyben.vinylManager.tools.log.VinylManagerLogger;
import java.awt.Desktop;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.net.*;
import java.nio.charset.Charset;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

/**
 * Classe d'outils divers.
 *
 * @author Mathieu Syben
 */
public class Tools {

    /**
     * Le charset par défaut.
     */
    public static final Charset defaultCharset = getDefaultCharset();

   /**
     * Récupère la trace d'une exception
     *
     * @param e l'exception
     * @return la trace de l'exception
     */
    public static String getStackTrace(Throwable e) {
        if (e == null) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        sb.append(e.getMessage());
        sb.append("\r\n");
        for (StackTraceElement ste : e.getStackTrace()) {
            sb.append(ste.getClassName());
            sb.append(".");
            sb.append(ste.getMethodName());
            sb.append(":");
            sb.append(ste.getLineNumber());
            sb.append("\r\n");
        }

        if (e.getCause() != null) {
            sb.append("\r\nCaused by\r\n");
            sb.append(getStackTrace(e.getCause()));
        }

        return sb.toString();
    }

    /**
     * Permet de lire un fichier
     *
     * @param f le fichier à lire
     * @return le contenu du fichier
     * @throws VinylManagerException En cas d'erreur.
     */
    public static byte[] getContenu(File f) throws VinylManagerException {
        InputStream is = null;
        ByteArrayOutputStream os = null;
        try {
            is = new FileInputStream(f);
            os = new ByteArrayOutputStream();

            copy(is, os);

            return os.toByteArray();
        } catch (Exception e) {
            throw new VinylManagerException(e);
        } finally {
            closeQuietly(is);
            closeQuietly(os);
        }

    }

    /**
     * Permet d'obtenir une version limité à 200pixels de l'image passée en
     * paramètre.
     *
     * @param imaArray L'image originale.
     * @return L'image réduite
     * @throws VinylManagerException En cas d'erreur.
     */
    public static byte[] getReducedImage(byte[] imaArray) throws VinylManagerException {
        return getReducedImage(imaArray, 200, 200);
    }
    
   /**
     * Permet d'obtenir une version limité à maxWidth ou maxHeight pixels de l'image passée en
     * paramètre. Si l'image est plus petite, elle n'est pas changée.
     *
     * @param imaArray L'image originale.
     * @param maxWidth la largeur max
     * @param maxHeight la hauteur max
     * @return L'image réduite
     * @throws VinylManagerException En cas d'erreur.
     */
     public static byte[] getReducedImage(byte[] imaArray, int maxWidth, int maxHeight) throws VinylManagerException {
        ByteArrayOutputStream os = null;
        try {
            BufferedImage photo = ImageIO.read(new ByteArrayInputStream(imaArray));

            double scale1 = (double) maxWidth / photo.getWidth();
            double scale2 = (double) maxHeight / photo.getHeight();

            double scaleValue = Math.min(scale1, scale2);
            if (scaleValue >= 1.0) {
                return imaArray;
            }
            AffineTransform tx = new AffineTransform();
            tx.scale(scaleValue, scaleValue);
            AffineTransformOp op = new AffineTransformOp(tx,
                    AffineTransformOp.TYPE_BICUBIC);
            BufferedImage photo_2 = new BufferedImage(
                    (int) (photo.getWidth() * scaleValue),
                    (int) (photo.getHeight() * scaleValue),
                    photo.getType());
            photo_2 = op.filter(photo, photo_2);
            os = new ByteArrayOutputStream();
            ImageIO.write(photo_2, "png", os);

            byte[] b = os.toByteArray();
            return b;
        } catch (IOException e) {
            throw new VinylManagerException(e);
        } catch (IllegalArgumentException e) {
            throw new VinylManagerException(e);
        } finally {
            closeQuietly(os);
            System.gc();
        }
    }
     
     
    
   /**
     * Permet d'obtenir une version limité à maxWidth ou maxHeight pixels de l'image passée en
     * paramètre. Si l'image est plus petite, elle est agrandie.
     *
     * @param imaArray L'image originale.
     * @param maxWidth la largeur max
     * @param maxHeight la hauteur max
     * @return L'image réduite
     * @throws VinylManagerException En cas d'erreur.
     */
     public static byte[] getResizedImage(byte[] imaArray, int maxWidth, int maxHeight) throws VinylManagerException {
        ByteArrayOutputStream os = null;
        try {
            BufferedImage photo = ImageIO.read(new ByteArrayInputStream(imaArray));

            double scale1 = (double) maxWidth / photo.getWidth();
            double scale2 = (double) maxHeight / photo.getHeight();

            double scaleValue = Math.min(scale1, scale2);
            AffineTransform tx = new AffineTransform();
            tx.scale(scaleValue, scaleValue);
            AffineTransformOp op = new AffineTransformOp(tx,
                    AffineTransformOp.TYPE_BICUBIC);
            BufferedImage photo_2 = new BufferedImage(
                    (int) (photo.getWidth() * scaleValue),
                    (int) (photo.getHeight() * scaleValue),
                    photo.getType());
            photo_2 = op.filter(photo, photo_2);
            os = new ByteArrayOutputStream();
            ImageIO.write(photo_2, "png", os);

            byte[] b = os.toByteArray();
            return b;
        } catch (IOException e) {
            throw new VinylManagerException(e);
        } catch (IllegalArgumentException e) {
            throw new VinylManagerException(e);
        } finally {
            closeQuietly(os);
            System.gc();
        }
    }

    /**
     * Permet d'obtenir une version limité à 80 de l'image passée en paramètre.
     *
     * @param imaArray L'image originale.
     * @return L'image réduite
     * @throws VinylManagerException En cas d'erreur.
     */
    public static byte[] getReducedImageForAndroid(byte[] imaArray) throws VinylManagerException {
        ByteArrayOutputStream os = null;
        try {

            imaArray = convertToRGB(imaArray);
            if (imaArray == null) {
                return null;
            }

            BufferedImage photo = ImageIO.read(new ByteArrayInputStream(imaArray));

            double scale1 = (double) 80 / photo.getWidth();
            double scale2 = (double) 80 / photo.getHeight();

            double scaleValue = Math.min(scale1, scale2);
            if (scaleValue >= 1.0) {
                return imaArray;
            }
            AffineTransform tx = new AffineTransform();
            tx.scale(scaleValue, scaleValue);
            AffineTransformOp op = new AffineTransformOp(tx,
                    AffineTransformOp.TYPE_BICUBIC);
            BufferedImage photo_2 = new BufferedImage(
                    (int) (photo.getWidth() * scaleValue),
                    (int) (photo.getHeight() * scaleValue),
                    photo.getType());
            photo_2 = op.filter(photo, photo_2);
            os = new ByteArrayOutputStream();
            ImageIO.write(photo_2, "png", os);

            byte[] b = os.toByteArray();
            return b;
        } catch (IOException e) {
            throw new VinylManagerException(e);
        } catch (IllegalArgumentException e) {
            throw new VinylManagerException(e);
        } finally {
            closeQuietly(os);
            System.gc();
        }
    }

    /**
     * Converti une image en RGB.
     *
     * @param source la source
     * @return l'image convertie.
     * @throws VinylManagerException En cas d'erreur.
     */
    public static byte[] convertToRGB(byte[] source) throws VinylManagerException {
        if (source == null || source.length == 0) {
            return null;
        }
        ByteArrayInputStream is = null;
        ByteArrayOutputStream os = null;
        try {
            is = new ByteArrayInputStream(source);
            BufferedImage photo = ImageIO.read(is);
            
            if (photo == null) {
                throw new VinylManagerException("null");
            }
            
            if (photo.getType() == BufferedImage.TYPE_CUSTOM) {

                BufferedImage photo_2 = new BufferedImage(
                        (photo.getWidth()),
                        (photo.getHeight()),
                        BufferedImage.TYPE_INT_RGB);
                for (int i = 0; i < photo.getWidth(); i++) {
                    for (int j = 0; j < photo.getHeight(); j++) {
                        photo_2.setRGB(i, j, photo.getRGB(i, j));
                    }
                }
                os = new ByteArrayOutputStream();
                ImageIO.write(photo_2, "png", os);


                byte[] b = os.toByteArray();
                return b;

            } else {
                return source;
            }
        } catch (Exception e) {
            throw new VinylManagerException(e);
        } finally {
            closeQuietly(is);
            closeQuietly(os);
            System.gc();
        }
    }

    /**
     * Renvoie le string passé en argument modifié pour être introduit dans du
     * html.
     *
     * @param s le string à modifier
     * @return le string modifié
     */
    public static String formatHtml(String s) {
        if (s == null) {
            return "";
        }
        return StringEscapeUtils.escapeHtml4(s);
    }

    /**
     * Ecrit dans un fichier
     *
     * @param f Le fichier dans lequel écrire
     * @param contenu le contenu à écrire
     */
    public static void writeFile(File f, byte[] contenu) {
        OutputStream os = null;
        try {
            os = new FileOutputStream(f);
            os.write(contenu);
            os.flush();
        } catch (Exception e) {
            VinylManagerLogger.log( e);
        } finally {
            closeQuietly(os);
        }
    }

    /**
     * Calcule la distance entre 2 String. 0 = identiques.
     *
     * @param from le premier String
     * @param to le second String
     * @return la distance entre les 2.
     */
    public static int distance(String from, String to) {
        if (from == null && to == null) {
            return 0;
        }

        if (to == null) {
            to = "";
        }

        if (from == null) {
            from = "";
        }
        
        if (to.toLowerCase().contains(from.toLowerCase())) {
            return 0;
        }

        return StringUtils.getLevenshteinDistance(from, to);
    }

    /**
     * Télécharge un fichier dont l'url est fournie
     *
     * @param url l'url du fichier à télécharger
     * @return le contenu du fichier.
     * @throws VinylManagerException En cas d'erreur.
     */
    public static byte[] downloadFile(String url) throws VinylManagerException {
        URL myurl;
        try {
            myurl = new URL(url);
        } catch (MalformedURLException ex) {
            throw new VinylManagerException(Language.getInstance().get("error.download"), ex);
        }
        URLConnection urlConnection;
        try {
            urlConnection = myurl.openConnection();
            urlConnection.setUseCaches(false);
        } catch (IOException ex) {
            throw new VinylManagerException(Language.getInstance().get("error.download"), ex);
        }
        try {
            urlConnection.connect();
        } catch (IOException ex) {
            throw new VinylManagerException(Language.getInstance().get("error.download"), ex);
        }
        InputStream input = null;
        ByteArrayOutputStream output = new ByteArrayOutputStream();

        try {
            //input = myurl.openStream();
            input = urlConnection.getInputStream();
            copy(input, output);
            return output.toByteArray();
        } catch (IOException ex) {
            throw new VinylManagerException(Language.getInstance().get("error.download"), ex);
        } finally {
            closeQuietly(input);
            closeQuietly(output);
        }


    }

    /**
     * Récupère le charset par défaut.
     *
     * @return le charset par défaut.
     */
    private static Charset getDefaultCharset() {
        Charset charset;
            try {
                charset = Charset.forName("UTF-8");
            } catch (IllegalArgumentException ex) {
                VinylManagerLogger.log( ex);
                charset = Charset.defaultCharset();
            }
        return charset;
    }

    /**
     * Récupère les informations mémoire sous forme de String.
     *
     * @return les informations mémoire sous forme de String.
     */
    public static String getMemoryUsage() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        StringBuilder contenu = new StringBuilder();
        contenu.append("HEAP MEMORY (application)");
        contenu.append("\r\n");
        contenu.append("\tinit : La taille initiale de la mémoire utilisé par l'application (généralement 0, sauf si l'on a utilisé l'option -Xms de la JVM).");
        contenu.append("\r\n");
        contenu.append("\tinit : ");
        contenu.append(convertInKM(memoryBean.getHeapMemoryUsage().getInit()));
        contenu.append("\r\n");
        contenu.append("\tused : La quantité de mémoire qui est réellement utilisée par votre application.");
        contenu.append("\r\n");
        contenu.append("\tused : ");
        contenu.append(convertInKM(memoryBean.getHeapMemoryUsage().getUsed()));
        contenu.append("\r\n");
        contenu.append("\tcommitted : La quantité de mémoire qui a été réservée auprès du système d'exploitation.");
        contenu.append("\r\n");
        contenu.append("\tcommited : ");
        contenu.append(convertInKM(memoryBean.getHeapMemoryUsage().getCommitted()));
        contenu.append("\r\n");
        contenu.append("\tmax : La quantité maximale que la JVM est authorisée à réserver auprès du système d'exploitation (modifiable avec l'option -Xmx de la JVM). Si l'application utilise plus de mémoire cela générera une OutOfMemoryError.");
        contenu.append("\r\n");
        contenu.append("\tmax : ");
        contenu.append(convertInKM(memoryBean.getHeapMemoryUsage().getMax()));
        contenu.append("\r\n");
        contenu.append("\r\n");
        contenu.append("\r\n");
        contenu.append("NON HEAP MEMORY (JVM)");
        contenu.append("\r\n");
        contenu.append("\tinit : ");
        contenu.append(convertInKM(memoryBean.getNonHeapMemoryUsage().getInit()));
        contenu.append("\r\n");
        contenu.append("\tused : ");
        contenu.append(convertInKM(memoryBean.getNonHeapMemoryUsage().getUsed()));
        contenu.append("\r\n");
        contenu.append("\tcommited : ");
        contenu.append(convertInKM(memoryBean.getNonHeapMemoryUsage().getCommitted()));
        contenu.append("\r\n");
        contenu.append("\tmax : ");
        contenu.append(convertInKM(memoryBean.getNonHeapMemoryUsage().getMax()));
        contenu.append("\r\n");
        return contenu.toString();
    }

    /**
     * Converti un long en K et en M afin de l'afficher de manière lisible.
     *
     * @param cont Le long à convertir.
     * @return Sa convertion.
     */
    private static String convertInKM(long cont) {
        double k = cont / 1024.0;
        double m = k / 1024.0;
        return (cont + " --> " + Math.round(k) + "K --> " + Math.round(m) + "M");
    }

    /**
     * Récupère une image depuis le clipboard.
     *
     * @return L'image s'il y en a une, null sinon.
     */
    public static BufferedImage getImageFromClipboard() {
        if (isImageInClipboard()) {
            Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
            if (t == null) {
                return null;
            }
            try {
                Image i = (Image) t.getTransferData(DataFlavor.imageFlavor);
                if (i instanceof BufferedImage) {
                    return (BufferedImage) i;
                } else {
                    BufferedImage newImage = new BufferedImage(i.getWidth(null), i.getHeight(null),
                BufferedImage.TYPE_INT_ARGB);
                    Graphics2D g = newImage.createGraphics();
                    g.drawImage(i, 0, 0, null);
                    g.dispose();
                    return newImage;
                }
                
            } catch (UnsupportedFlavorException | IOException ex) {
                VinylManagerLogger.log( ex);
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Met une image dans le clipboard.
     *
     * @param icon L'image à mettre.
     */
    public static void setClipboard(Icon icon) {
        if (icon instanceof ImageIcon) {
            setClipboard(((ImageIcon) icon).getImage());
        } else {
            BufferedImage image = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_RGB);
            icon.paintIcon(null, image.getGraphics(), 0, 0);
            setClipboard(image);
        }
    }

    /**
     * Met une image dans le clipboard.
     *
     * @param image L'image à mettre.
     */
    public static void setClipboard(java.awt.Image image) {
        ImageSelection imgSel = new ImageSelection(image);
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(imgSel, null);
    }
    
    /**
     * Vide le clipboard.
     */
    public static void clearClipboard() {
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(null, null);
    }

    /**
     * Permet de savoir s'il y a une image dans le clipboard.
     *
     * @return true s'il y en a une, false sinon.
     */
    public static boolean isImageInClipboard() {
        Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
        return (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor));
    }

    /**
     * Ouvre une url dans un browser.
     *
     * @param url l'url à ouvrir.
     */
    public static void openUrl(String url) {
        if (url == null) {
            return;
        }
        if (Desktop.isDesktopSupported()) {
            Desktop desktop = Desktop.getDesktop();
            if (desktop.isSupported(Desktop.Action.BROWSE)) {
                try {
                    desktop.browse(new URI(url));
                } catch (URISyntaxException ex) {
                    VinylManagerLogger.log( ex);
                } catch (IOException ex) {
                    VinylManagerLogger.log( ex);
                }
            }
        }
    }

    /**
     * Récupère la durée. 0 en cas de paramètre null ou vide.
     *
     * @param text le texte à parser
     * @throws VinylManagerException en cas d'erreur
     */
    public static int getDuree(String text) throws VinylManagerException {
        if (text == null || text.trim().equals("")) {
            return 0;
        }

        if (text.contains(":") || text.contains(".")) {
            String[] sep;

            if (text.contains(":")) {
                sep = text.split(":");
            } else {
                sep = text.split("\\.");
            }

            if (sep.length != 2) {
                throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"));
            }
            int min = 0;
            int sec = 0;
            try {
                min = Integer.parseInt(sep[0]);
                sec = Integer.parseInt(sep[1]);
            } catch (NumberFormatException ex) {
                throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"), ex);
            }
            if (min < 0) {
                throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"));
            }
            if (sec < 0) {
                throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"));
            }
            if (sec >= 60) {
                throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"));
            }

            return min * 60 + sec;
        }

        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException ex) {
            throw new VinylManagerException(Language.getInstance().get("error.number.duree.conversion"), ex);
        }

    }

    /**
     * Ouvre un fichier externe avec l'application par défaut de l'os.
     * @param path Le chemin du fichier à ouvrir.
     * @throws VinylManagerException En cas d'erreur.
     */
    public static void openFile(String path) throws VinylManagerException {
        if (Desktop.isDesktopSupported()) {
            if (path == null || path.isEmpty()) {
                throw new VinylManagerException();
            }
            File f = new File(path);
            if (!f.exists() || !f.isFile()) {
                throw new VinylManagerException(Language.getInstance().get("error.file.not.exists").replace("%1", f.getAbsolutePath()));
            } else {
                try {
                    Desktop.getDesktop().open(f);
                } catch (Exception ex) {
                    throw new VinylManagerException(Language.getInstance().get("error.open.external.file"), ex);
                }
            }
        } else {
            throw new VinylManagerException(Language.getInstance().get("error.open.external.file"));
        }
    }
    
    /**
     * Ferme un Closeable silencieusement
     */
    public static void closeQuietly(Closeable c) {
        if (c != null) {
            try {
                c.close();
            } catch (IOException e) {
                
            }
        }
    }
    /**
     * Copie un stream
     */
    public static void copy(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[8 * 1024];
        int len;
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }
    }
    
    /**
     * Ecrit dans un fichier
     */
    public static void writeByteArrayToFile(File f, byte[] content) throws IOException {
        OutputStream os = null;
        try {
            os = new FileOutputStream(f);
            os.write(content);
        } finally {
            closeQuietly(os);
        }
    }
    
    /**
     * Lit un fichier
     */
    public static byte[] readFile(File f) throws IOException {
        InputStream is = null;
        ByteArrayOutputStream os = null;
        try {
            is = new FileInputStream(f);
            os = new ByteArrayOutputStream();
            copy(is, os);
            return os.toByteArray();
        } finally {
            closeQuietly(is);
            closeQuietly(os);
        }
    }
}
