// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.javaFX.fxml.codeInsight;

import com.intellij.java.JavaBundle;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ElementColorProvider;
import com.intellij.psi.*;
import com.intellij.psi.impl.JavaConstantExpressionEvaluator;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.javaFX.fxml.JavaFxCommonNames;

import java.awt.*;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.IntFunction;

public final class JavaFxColorProvider implements ElementColorProvider {
  private static final String COLOR = "color";
  private static final String RGB = "rgb";
  private static final String GRAY = "gray";
  private static final String GRAY_RGB = "grayRgb";
  private static final String HSB = "hsb";
  private static final Set<String> FACTORY_METHODS = Set.of(COLOR, RGB, GRAY, GRAY_RGB, HSB);
  private static final String DECIMAL_FORMAT_PATTERN = "#.####";
  private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.US); // use '.' as a decimal separator

  @Override
  public Color getColorFrom(@NotNull PsiElement element) {
    if (!(element instanceof PsiIdentifier)) return null;
    PsiElement parent = element.getParent();
    PsiElement gp = parent == null ? null : parent.getParent();
    if (gp instanceof PsiNewExpression newExpression && newExpression.getClassReference() == parent) {
      if (isColorClass(PsiTypesUtil.getPsiClass(newExpression.getType()))) {
        PsiExpressionList argumentList = newExpression.getArgumentList();
        if (argumentList != null) {
          PsiExpression[] args = argumentList.getExpressions();
          if (args.length == 4) {
            Object[] values = getArgumentValues(args);
            return getScaledRgbColor(values[0], values[1], values[2], values[3]);
          }
        }
      }
    }
    while (parent instanceof PsiReferenceExpression && parent.getParent() instanceof PsiReferenceExpression) {
      parent = parent.getParent();
      gp = parent.getParent();
    }
    if (gp instanceof PsiMethodCallExpression methodCall && methodCall.getMethodExpression().getReferenceNameElement() == element) {
      PsiReferenceExpression methodExpression = methodCall.getMethodExpression();
      String methodName = methodExpression.getReferenceName();
      if (FACTORY_METHODS.contains(methodName)) {
        PsiElement resolved = methodExpression.resolve();
        if (resolved instanceof PsiMethod method) {
          if (method.hasModifierProperty(PsiModifier.STATIC)) {
            if (isColorClass(method.getContainingClass())) {
              return getColor(methodName, methodCall.getArgumentList());
            }
          }
        }
      }
    }
    return null;
  }

  private static boolean isColorClass(@Nullable PsiClass aClass) {
    return aClass != null && JavaFxCommonNames.JAVAFX_SCENE_COLOR.equals(aClass.getQualifiedName());
  }

  private static @Nullable Color getColor(@Nullable String methodName, @NotNull PsiExpressionList argumentList) {
    if (methodName == null) return null;
    Object[] values = getArgumentValues(argumentList.getExpressions());
    return switch (methodName) {
      case COLOR -> switch (values.length) {
        case 4 -> getScaledRgbColor(values[0], values[1], values[2], values[3]);
        case 3 -> getScaledRgbColor(values[0], values[1], values[2], Double.valueOf(1));
        default -> null;
      };
      case RGB -> switch (values.length) {
        case 4 -> getRgbColor(values[0], values[1], values[2], values[3]);
        case 3 -> getRgbColor(values[0], values[1], values[2], Double.valueOf(1));
        default -> null;
      };
      case GRAY -> switch (values.length) {
        case 2 -> getScaledRgbColor(values[0], values[0], values[0], values[1]);
        case 1 -> getScaledRgbColor(values[0], values[0], values[0], Double.valueOf(1));
        default -> null;
      };
      case GRAY_RGB -> switch (values.length) {
        case 2 -> getRgbColor(values[0], values[0], values[0], values[1]);
        case 1 -> getRgbColor(values[0], values[0], values[0], Double.valueOf(1));
        default -> null;
      };
      case HSB -> switch (values.length) {
        case 4 -> getHsbColor(values[0], values[1], values[2], values[3]);
        case 3 -> getHsbColor(values[0], values[1], values[2], Double.valueOf(1));
        default -> null;
      };
      default -> null;
    };
  }

  private static Object @NotNull [] getArgumentValues(PsiExpression @NotNull [] argumentExpressions) {
    return ContainerUtil.map(argumentExpressions,
                             expression -> JavaConstantExpressionEvaluator.computeConstantExpression(expression, false),
                             ArrayUtilRt.EMPTY_OBJECT_ARRAY);
  }

  private static @Nullable Color getScaledRgbColor(@Nullable Object redValue,
                                                   @Nullable Object greenValue,
                                                   @Nullable Object blueValue,
                                                   @Nullable Object alphaValue) {
    Integer red = getScaledComponent(redValue);
    Integer green = getScaledComponent(greenValue);
    Integer blue = getScaledComponent(blueValue);
    Integer alpha = getScaledComponent(alphaValue);
    if (red != null && green != null && blue != null && alpha != null) {
      //noinspection UseJBColor
      return new Color(red, green, blue, alpha);
    }
    return null;
  }

  private static @Nullable Color getRgbColor(@Nullable Object redValue,
                                             @Nullable Object greenValue,
                                             @Nullable Object blueValue,
                                             @Nullable Object alphaValue) {
    Integer red = getComponent(redValue);
    Integer green = getComponent(greenValue);
    Integer blue = getComponent(blueValue);
    Integer alpha = getScaledComponent(alphaValue);
    if (red != null && green != null && blue != null && alpha != null) {
      //noinspection UseJBColor
      return new Color(red, green, blue, alpha);
    }
    return null;
  }

  private static Integer getComponent(Object value) {
    if (value instanceof Number) {
      int component = ((Number)value).intValue();
      if (component >= 0 && component <= 255) {
        return component;
      }
    }
    return null;
  }

  private static Integer getScaledComponent(Object value) {
    if (value instanceof Number) {
      double doubleValue = ((Number)value).doubleValue();
      int component = (int)(doubleValue * 255 + 0.5);
      if (component >= 0 && component <= 255) {
        return component;
      }
    }
    return null;
  }

  private static Float getHsbComponent(Object value, boolean checkRange) {
    if (value instanceof Number) {
      float component = ((Number)value).floatValue();
      if (!checkRange || component >= 0.0f && component <= 1.0f) {
        return component;
      }
    }
    return null;
  }

  @Override
  public void setColorTo(@NotNull PsiElement element, @NotNull Color color) {
    Runnable command = null;
    if (element instanceof PsiNewExpression expr) {
      PsiExpressionList argumentList = expr.getArgumentList();
      assert argumentList != null;
      command = () -> replaceConstructorArgs(color, argumentList);
    }
    if (element instanceof PsiMethodCallExpression methodCall) {
      PsiReferenceExpression methodExpression = methodCall.getMethodExpression();
      String methodName = methodExpression.getReferenceName();
      if (COLOR.equals(methodName) || GRAY.equals(methodName)) {
        command = () -> replaceColor(methodCall, getScaledRgbCallText(color));
      }
      else if (RGB.equals(methodName) || GRAY_RGB.equals(methodName)) {
        command = () -> replaceColor(methodCall, getRgbCallText(color));
      }
      else if (HSB.equals(methodName)) {
        command = () -> replaceColor(methodCall, getHsbCallText(color));
      }
    }

    if (command != null) {
      Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(element.getContainingFile());
      CommandProcessor.getInstance()
        .executeCommand(element.getProject(), command, JavaBundle.message("change.color.command.text"), null, document);
    }
  }

  private static void replaceConstructorArgs(@NotNull Color color, PsiExpressionList argumentList) {
    PsiElementFactory factory = JavaPsiFacade.getElementFactory(argumentList.getProject());
    String text = JavaFxCommonNames.JAVAFX_SCENE_COLOR + "(" +
                  formatScaledComponent(color.getRed()) + "," +
                  formatScaledComponent(color.getGreen()) + "," +
                  formatScaledComponent(color.getBlue()) + "," +
                  formatScaledComponent(color.getAlpha()) + ")";
    PsiMethodCallExpression newCall = (PsiMethodCallExpression)factory.createExpressionFromText(text, argumentList);
    argumentList.replace(newCall.getArgumentList());
  }

  private static void replaceColor(@NotNull PsiMethodCallExpression methodCall, @NotNull String callText) {
    PsiElementFactory factory = JavaPsiFacade.getElementFactory(methodCall.getProject());
    PsiMethodCallExpression newCall = (PsiMethodCallExpression)factory.createExpressionFromText(callText, methodCall);
    methodCall.getArgumentList().replace(newCall.getArgumentList());

    PsiElement nameElement = methodCall.getMethodExpression().getReferenceNameElement();
    assert nameElement != null;
    PsiElement newNameElement = newCall.getMethodExpression().getReferenceNameElement();
    assert newNameElement != null;
    nameElement.replace(newNameElement);
  }

  private static Color getHsbColor(Object hValue, Object sValue, Object bValue, Object alphaValue) {
    Float h = getHsbComponent(hValue, false);
    Float s = getHsbComponent(sValue, true);
    Float b = getHsbComponent(bValue, true);
    Integer alpha = getScaledComponent(alphaValue);
    if (h != null && s != null && b != null && alpha != null) {
      Color hsbColor = Color.getHSBColor(h / 360.0f, s, b);
      //noinspection UseJBColor
      return alpha == 255 ? hsbColor : new Color(hsbColor.getRed(), hsbColor.getGreen(), hsbColor.getBlue(), alpha);
    }
    return null;
  }

  private static @NotNull String getScaledRgbCallText(@NotNull Color color) {
    return getCallText(color, COLOR, GRAY, JavaFxColorProvider::formatScaledComponent);
  }

  private static @NotNull String getRgbCallText(@NotNull Color color) {
    return getCallText(color, RGB, GRAY_RGB, String::valueOf);
  }

  private static @NotNull String getCallText(@NotNull Color color, String colorMethodName, String grayMethodName, IntFunction<String> formatter) {
    String methodName;
    StringJoiner args = new StringJoiner(",", "(", ")");
    if (color.getRed() == color.getGreen() && color.getRed() == color.getBlue()) {
      methodName = grayMethodName;
      args.add(formatter.apply(color.getRed()));
    }
    else {
      methodName = colorMethodName;
      args.add(formatter.apply(color.getRed()));
      args.add(formatter.apply(color.getGreen()));
      args.add(formatter.apply(color.getBlue()));
    }
    if (color.getAlpha() != 255) {
      args.add(formatScaledComponent(color.getAlpha()));
    }
    return methodName + args;
  }

  private static @NotNull String formatScaledComponent(int colorComponent) {
    DecimalFormat df = new DecimalFormat(DECIMAL_FORMAT_PATTERN, DECIMAL_FORMAT_SYMBOLS); // not thread safe - can't have a constant
    return df.format(colorComponent / 255.0);
  }

  private static String getHsbCallText(Color color) {
    float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
    DecimalFormat df = new DecimalFormat(DECIMAL_FORMAT_PATTERN, DECIMAL_FORMAT_SYMBOLS);
    StringJoiner args = new StringJoiner(",", "(", ")");
    args.add(df.format(hsb[0] * 360));
    args.add(df.format(hsb[1]));
    args.add(df.format(hsb[2]));
    if (color.getAlpha() != 255) {
      args.add(formatScaledComponent(color.getAlpha()));
    }
    return HSB + args;
  }
}
