/*
 * Copyright 2020 Laszlo Balazs-Csiki and Contributors
 *
 * This file is part of Pixelitor. Pixelitor is free software: you
 * can redistribute it and/or modify it under the terms of the GNU
 * General Public License, version 3 as published by the Free
 * Software Foundation.
 *
 * Pixelitor 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>.
 */

package pixelitor.utils.debug;

import pixelitor.Canvas;
import pixelitor.Composition;
import pixelitor.NewImage;
import pixelitor.OpenImages;
import pixelitor.colors.FillType;
import pixelitor.filters.Filter;
import pixelitor.gui.PixelitorWindow;
import pixelitor.gui.View;
import pixelitor.gui.utils.GUIUtils;
import pixelitor.layers.*;
import pixelitor.tools.Tools;
import pixelitor.tools.pen.Path;
import pixelitor.utils.*;

import javax.swing.*;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.NORTH;
import static javax.swing.BorderFactory.createEmptyBorder;
import static pixelitor.OpenImages.addAsNewComp;
import static pixelitor.OpenImages.findCompByName;
import static pixelitor.tools.pen.PenToolMode.EDIT;
import static pixelitor.utils.Threads.calledOutsideEDT;

/**
 * Debugging-related static utility methods
 */
public class Debug {
    private Debug() {
        // shouldn't be instantiated
    }

    public static String dateBufferTypeAsString(int type) {
        switch (type) {
            case DataBuffer.TYPE_BYTE:
                return "BYTE";
            case DataBuffer.TYPE_USHORT:
                return "USHORT";
            case DataBuffer.TYPE_SHORT:
                return "SHORT";
            case DataBuffer.TYPE_INT:
                return "INT";
            case DataBuffer.TYPE_FLOAT:
                return "FLOAT";
            case DataBuffer.TYPE_DOUBLE:
                return "DOUBLE";
            case DataBuffer.TYPE_UNDEFINED:
                return "UNDEFINED";
            default:
                return "unrecognized (" + type + ")";
        }
    }

    static String transparencyAsString(int transparency) {
        switch (transparency) {
            case Transparency.OPAQUE:
                return "OPAQUE";
            case Transparency.BITMASK:
                return "BITMASK";
            case Transparency.TRANSLUCENT:
                return "TRANSLUCENT";
            default:
                return "unrecognized (" + transparency + ")";
        }
    }

    public static String bufferedImageTypeAsString(int type) {
        switch (type) {
            case BufferedImage.TYPE_3BYTE_BGR:
                return "3BYTE_BGR";
            case BufferedImage.TYPE_4BYTE_ABGR:
                return "4BYTE_ABGR";
            case BufferedImage.TYPE_4BYTE_ABGR_PRE:
                return "4BYTE_ABGR_PRE";
            case BufferedImage.TYPE_BYTE_BINARY:
                return "BYTE_BINARY";
            case BufferedImage.TYPE_BYTE_GRAY:
                return "BYTE_GRAY";
            case BufferedImage.TYPE_BYTE_INDEXED:
                return "BYTE_INDEXED";
            case BufferedImage.TYPE_CUSTOM:
                return "CUSTOM";
            case BufferedImage.TYPE_INT_ARGB:
                return "INT_ARGB";
            case BufferedImage.TYPE_INT_ARGB_PRE:
                return "INT_ARGB_PRE";
            case BufferedImage.TYPE_INT_BGR:
                return "INT_BGR";
            case BufferedImage.TYPE_INT_RGB:
                return "INT_RGB";
            case BufferedImage.TYPE_USHORT_555_RGB:
                return "USHORT_555_RGB";
            case BufferedImage.TYPE_USHORT_565_RGB:
                return "USHORT_565_RGB";
            case BufferedImage.TYPE_USHORT_GRAY:
                return "USHORT_GRAY";
            default:
                return "unrecognized (" + type + ")";
        }
    }

    static String colorSpaceTypeAsString(int type) {
        switch (type) {
            case ColorSpace.TYPE_2CLR:
                return "2CLR";
            case ColorSpace.TYPE_3CLR:
                return "3CLR";
            case ColorSpace.TYPE_4CLR:
                return "4CLR";
            case ColorSpace.TYPE_5CLR:
                return "5CLR";
            case ColorSpace.TYPE_6CLR:
                return "6CLR";
            case ColorSpace.TYPE_7CLR:
                return "7CLR";
            case ColorSpace.TYPE_8CLR:
                return "8CLR";
            case ColorSpace.TYPE_9CLR:
                return "9CLR";
            case ColorSpace.TYPE_ACLR:
                return "ACLR";
            case ColorSpace.TYPE_BCLR:
                return "BCLR";
            case ColorSpace.TYPE_CCLR:
                return "CCLR";
            case ColorSpace.TYPE_CMY:
                return "CMY";
            case ColorSpace.TYPE_CMYK:
                return "CMYK";
            case ColorSpace.TYPE_DCLR:
                return "DCLR";
            case ColorSpace.TYPE_ECLR:
                return "ECLR";
            case ColorSpace.TYPE_FCLR:
                return "FCLR";
            case ColorSpace.TYPE_GRAY:
                return "GRAY";
            case ColorSpace.TYPE_HLS:
                return "HLS";
            case ColorSpace.TYPE_HSV:
                return "HSV";
            case ColorSpace.TYPE_Lab:
                return "Lab";
            case ColorSpace.TYPE_Luv:
                return "Luv";
            case ColorSpace.TYPE_RGB:
                return "RGB";
            case ColorSpace.TYPE_XYZ:
                return "XYZ";
            case ColorSpace.TYPE_YCbCr:
                return "YCbCr";
            case ColorSpace.TYPE_Yxy:
                return "Yxy";
            default:
                return "unrecognized (" + type + ")";
        }
    }

    public static boolean isRgbColorModel(ColorModel cm) {
        if (cm instanceof DirectColorModel
            && cm.getTransferType() == DataBuffer.TYPE_INT) {

            var dcm = (DirectColorModel) cm;
            return dcm.getRedMask() == 0x00FF0000
                && dcm.getGreenMask() == 0x0000FF00
                && dcm.getBlueMask() == 0x000000FF
                && (dcm.getNumComponents() == 3 || dcm.getAlphaMask() == 0xFF000000);
        }

        return false;
    }

    public static boolean isBgrColorModel(ColorModel cm) {
        if (cm instanceof DirectColorModel &&
            cm.getTransferType() == DataBuffer.TYPE_INT) {

            var dcm = (DirectColorModel) cm;
            return dcm.getRedMask() == 0x000000FF
                && dcm.getGreenMask() == 0x0000FF00
                && dcm.getBlueMask() == 0x00FF0000
                && (dcm.getNumComponents() == 3 || dcm.getAlphaMask() == 0xFF000000);
        }

        return false;
    }

    public static String mouseModifiers(MouseEvent e) {
        boolean altDown = e.isAltDown();
        boolean controlDown = e.isControlDown();
        boolean shiftDown = e.isShiftDown();
        boolean rightMouse = SwingUtilities.isRightMouseButton(e);
        StringBuilder msg = new StringBuilder(25);
        if (controlDown) {
            msg.append(Ansi.red("ctrl-"));
        }
        if (altDown) {
            msg.append(Ansi.green("alt-"));
        }
        if (shiftDown) {
            msg.append(Ansi.blue("shift-"));
        }
        if (rightMouse) {
            msg.append(Ansi.yellow("right-"));
        }
        if (e.isPopupTrigger()) {
            msg.append(Ansi.cyan("popup-"));
        }
        return msg.toString();
    }

    public static void call(String... args) {
        call(false, args);
    }

    public static void callAndCaller(String... args) {
        call(true, args);
    }

    private static void call(boolean printCaller, String... args) {
        StackTraceElement[] fullTrace = new Throwable().getStackTrace();
        StackTraceElement me = fullTrace[2];

        String threadName;
        if (Threads.calledOnEDT()) {
            threadName = "EDT";
        } else {
            threadName = Threads.threadName();
        }

        String argsAsOneString;
        if (args.length == 0) {
            argsAsOneString = "";
        } else if (args.length == 1) {
            argsAsOneString = args[0];
        } else {
            argsAsOneString = Arrays.toString(args);
        }

        String className = me.getClassName();
        // strip the package name
        className = className.substring(className.lastIndexOf('.') + 1);

        if (printCaller) {
            StackTraceElement caller = fullTrace[3];
            String callerClassName = caller.getClassName();
            callerClassName = callerClassName.substring(callerClassName.lastIndexOf('.') + 1);

            System.out.printf("%s.%s(%s) on %s by %s.%s%n", className,
                me.getMethodName(), Ansi.yellow(argsAsOneString),
                threadName, callerClassName, caller.getMethodName());
        } else {
            System.out.printf("%s.%s(%s) on %s%n", className,
                me.getMethodName(), Ansi.yellow(argsAsOneString), threadName);
        }
    }

    public static String keyEvent(KeyEvent e) {
        String keyText = KeyEvent.getKeyText(e.getKeyCode());
        int modifiers = e.getModifiersEx();
        if (modifiers != 0) {
            String modifiersText = InputEvent.getModifiersExText(modifiers);
            if (keyText.equals(modifiersText)) { // the key itself is the modifier
                return modifiersText;
            }
            return modifiersText + "+" + keyText;
        }
        return keyText;
    }

    public static void image(Image img) {
        image(img, "Debug");
    }

    public static void image(Image img, String name) {
        // make sure the this is called on the EDT
        if (calledOutsideEDT()) {
            EventQueue.invokeLater(() -> image(img, name));
            return;
        }

        BufferedImage copy = ImageUtils.copyToBufferedImage(img);

        View previousView = OpenImages.getActiveView();

        findCompByName(name).ifPresentOrElse(
            comp -> replaceImageInDebugComp(comp, copy),
            () -> addAsNewComp(copy, null, name));

        if (previousView != null) {
            OpenImages.setActiveView(previousView, true);
        }
    }

    private static void replaceImageInDebugComp(Composition comp, BufferedImage copy) {
        Canvas canvas = comp.getCanvas();
        comp.getActiveDrawableOrThrow().setImage(copy);

        if (canvas.hasDifferentSizeThan(copy)) {
            canvas.changeSize(copy.getWidth(), copy.getHeight(), comp.getView());
        }

        comp.repaint();
    }

    public static void shape(Shape shape, String name) {
        // create a copy
        Path2D shapeCopy = new Path2D.Double(shape);

        Rectangle shapeBounds = shape.getBounds();
        int imgWidth = shapeBounds.x + shapeBounds.width + 50;
        int imgHeight = shapeBounds.y + shapeBounds.height + 50;

        BufferedImage debugImg = ImageUtils.createSysCompatibleImage(imgWidth, imgHeight);
        Drawer.on(debugImg)
            .fillWith(Color.WHITE)
            .useAA()
            .draw(g -> {
                g.setColor(Color.BLACK);
                g.setStroke(new BasicStroke(3));
                g.draw(shapeCopy);
            });

        image(debugImg, name);
    }

    public static void raster(Raster raster, String name) {
        ColorModel colorModel;
        int numBands = raster.getNumBands();

        if (numBands == 4) { // normal color image
            colorModel = new DirectColorModel(
                ColorSpace.getInstance(ColorSpace.CS_sRGB), 32,
                0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000,
                true, DataBuffer.TYPE_INT);
        } else if (numBands == 1) { // grayscale image
            int[] nBits = {8};
            colorModel = new ComponentColorModel(
                ColorSpace.getInstance(ColorSpace.CS_GRAY), nBits,
                false, true,
                Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
        } else {
            throw new IllegalStateException("numBands = " + numBands);
        }

        Raster correctlyTranslated = raster.createChild(
            raster.getMinX(), raster.getMinY(),
            raster.getWidth(), raster.getHeight(),
            0, 0, null);
        BufferedImage debugImage = new BufferedImage(colorModel,
            (WritableRaster) correctlyTranslated, true, null);

        image(debugImage, name);
    }

    public static void rasterWithEmptySpace(Raster raster) {
        BufferedImage debugImage = new BufferedImage(
            raster.getMinX() + raster.getWidth(),
            raster.getMinY() + raster.getHeight(),
            BufferedImage.TYPE_4BYTE_ABGR_PRE);
        debugImage.setData(raster);

        image(debugImage);
    }

    public static void keepSwitchingToolsRandomly() {
        Runnable backgroundTask = () -> {
            //noinspection InfiniteLoopStatement
            while (true) {
                Utils.sleep(1, TimeUnit.SECONDS);

                Runnable changeToolOnEDTTask = () -> Tools.getRandomTool().activate();
                GUIUtils.invokeAndWait(changeToolOnEDTTask);
            }
        };
        new Thread(backgroundTask).start();
    }

    public static void dispatchKeyPress(PixelitorWindow pw, boolean ctrl, int keyCode, char keyChar) {
        int modifiers;
        if (ctrl) {
            modifiers = InputEvent.CTRL_DOWN_MASK;
        } else {
            modifiers = 0;
        }
        pw.dispatchEvent(new KeyEvent(pw, KeyEvent.KEY_PRESSED,
            System.currentTimeMillis(), modifiers, keyCode, keyChar));
    }

    public static void addTestPath() {
        var shape = new Rectangle2D.Double(100, 100, 300, 100);

        Path path = Shapes.shapeToPath(shape, OpenImages.getActiveView());

        Tools.PEN.setPath(path);
        Tools.PEN.startRestrictedMode(EDIT, false);
        Tools.PEN.activate();
    }

    public static void showAddTextLayerDialog() {
        AddTextLayerAction.INSTANCE.actionPerformed(null);
    }

    public static void addMaskAndShowIt() {
        AddLayerMaskAction.INSTANCE.actionPerformed(null);
        View view = OpenImages.getActiveView();
        Layer layer = view.getComp().getActiveLayer();
        MaskViewMode.SHOW_MASK.activate(view, layer);
    }

    public static void startFilter(Filter filter) {
        filter.startOn(OpenImages.getActiveDrawableOrThrow());
    }

    public static void addNewImageWithMask() {
        NewImage.addNewImage(FillType.WHITE, 600, 400, "Test");
        OpenImages.getActiveLayer().addMask(LayerMaskAddType.PATTERN);
    }

    public static void showInternalState() {
        AppNode node = new AppNode();
        String title = "Internal State";

        JTree tree = new JTree(node);

        JLabel explainLabel = new JLabel(
            // TODO re-formulate
            "<html>If you are reporting a bug that cannot be reproduced," +
                "<br>please include the following information:");
        explainLabel.setBorder(createEmptyBorder(5, 5, 5, 5));

        JPanel form = new JPanel(new BorderLayout());
        form.add(explainLabel, NORTH);
        form.add(new JScrollPane(tree), CENTER);

        String text = node.toDetailedString();

        GUIUtils.showCopyTextToClipboardDialog(form, text, title);
    }
}