package net.ranides.assira.ui.parsers;

import lombok.experimental.UtilityClass;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.grammar.G4Node;
import net.ranides.assira.grammar.G4Selector;
import net.ranides.assira.grammar.G4SelectorBuilder;
import net.ranides.assira.ui.model.CssUnitAngle;
import net.ranides.assira.ui.model.CssUnit;
import net.ranides.grammars.generated.UiGrammarParser;

import javax.swing.*;
import java.awt.*;

@UtilityClass
public class ParseCssUnit {

    public static final G4Selector<CssUnit> UNIT_LENGTH = new G4SelectorBuilder<CssUnit>()
        .withName("CssUnit:UNIT_LENGTH")
        .withParser(GrammarBase.PARSER)
        .valueForToken(UiGrammarParser.CSS2_UNIT_PX, CssUnit.PX)
        .valueForToken(UiGrammarParser.CSS2_UNIT_PT, CssUnit.PT)
        .valueForToken(UiGrammarParser.CSS2_UNIT_CM, CssUnit.CM)
        .valueForToken(UiGrammarParser.CSS2_UNIT_MM, CssUnit.MM)
        .valueForToken(UiGrammarParser.CHAR_Q, CssUnit.Q)
        .valueForToken(UiGrammarParser.CSS2_UNIT_IN, CssUnit.IN)
        .valueForToken(UiGrammarParser.CSS2_UNIT_PC, CssUnit.PC)

        .valueForToken(UiGrammarParser.CSS2_UNIT_EM, CssUnit.EM)
        .valueForToken(UiGrammarParser.CSS2_UNIT_EX, CssUnit.EX)
        .valueForToken(UiGrammarParser.CSS2_UNIT_CAP, CssUnit.CAP)
        .valueForToken(UiGrammarParser.CSS2_UNIT_CH, CssUnit.CH)
        .valueForToken(UiGrammarParser.CSS2_UNIT_IC, CssUnit.IC)
        .valueForToken(UiGrammarParser.CSS2_UNIT_LH, CssUnit.LH)

        .valueForToken(UiGrammarParser.CSS2_UNIT_REM, CssUnit.REM)
        .valueForToken(UiGrammarParser.CSS2_UNIT_REX, CssUnit.REX)
        .valueForToken(UiGrammarParser.CSS2_UNIT_RCAP, CssUnit.RCAP)
        .valueForToken(UiGrammarParser.CSS2_UNIT_RCH, CssUnit.RCH)
        .valueForToken(UiGrammarParser.CSS2_UNIT_RIC, CssUnit.RIC)
        .valueForToken(UiGrammarParser.CSS2_UNIT_RLH, CssUnit.RLH)

        .valueForToken(UiGrammarParser.CSS2_UNIT_VB, CssUnit.VB)
        .valueForToken(UiGrammarParser.CSS2_UNIT_VI, CssUnit.VI)
        .valueForToken(UiGrammarParser.CSS2_UNIT_VW, CssUnit.VW)
        .valueForToken(UiGrammarParser.CSS2_UNIT_VH, CssUnit.VH)
        .valueForToken(UiGrammarParser.CSS2_UNIT_VMAX, CssUnit.VMAX)
        .valueForToken(UiGrammarParser.CSS2_UNIT_VMIN, CssUnit.VMIN)
        .prepare();

    public static final G4Selector<CssUnitAngle> UNIT_ANGLE = new G4SelectorBuilder<CssUnitAngle>()
        .withName("CssUnit:UNIT")
        .withParser(GrammarBase.PARSER)
        .valueForToken(UiGrammarParser.CSS4_DEG, CssUnitAngle.DEG)
        .valueForToken(UiGrammarParser.CSS4_RAD, CssUnitAngle.RAD)
        .valueForToken(UiGrammarParser.CSS4_GRAD, CssUnitAngle.GRAD)
        .valueForToken(UiGrammarParser.CSS4_TURN, CssUnitAngle.TURN)
        .prepare();

    public static final G4Selector<Integer> LENGTH = new G4SelectorBuilder<Integer>()
        .withName("CssUnit:LENGTH")
        .withParser(GrammarBase.PARSER)
        .mapRule(UiGrammarParser.RULE_length, node -> {
            Number number = ParsePrimitives.parseNumber(node.child("number"));
            CssUnit unit = UNIT_LENGTH.single(node.child("unitLength"));
            return Math.round(length(node, number, unit).floatValue());

        })
        .prepare();

    public static final G4Selector<Double> ANGLE = new G4SelectorBuilder<Double>()
        .withName("CssUnit:ANGLE")
        .withParser(GrammarBase.PARSER)
        .mapRule(UiGrammarParser.RULE_angle, node -> {
            Number number = ParsePrimitives.parseNumber(node.child("number"));
            CssUnitAngle unit = UNIT_ANGLE.optional(node).orElse(CssUnitAngle.DEG);
            return angle(number, unit).doubleValue();
        })
        .prepare();

    public static int length(JComponent component, String value) {
        return GrammarBase.parseSingle(LENGTH, "length", component, value);
    }

    public static int length(String value) {
        return GrammarBase.parseSingle(LENGTH, "length", value);
    }

    public static Number length(G4Node node, Number number, CssUnit unit) {
        double v = number.doubleValue();

        switch (unit) {
            case PX:
                return number;
            case PT:
                return 1.3333 * v;
            case CM:
                return 37.8000 * v;
            case MM:
                return 3.7800 * v;
            case Q:
                return 0.9450 * v;
            case IN:
                return 96.0000 * v;
            case PC:
                return 16.0000 * v;

            case EM:
                return v * computeEm(GrammarBase.component(node));
            case EX:
                return v * computeEx(GrammarBase.component(node));
            case CAP:
                return v * computeCap(GrammarBase.component(node));
            case CH:
                return v * computeCh(GrammarBase.component(node));
            case IC:
                return v * computeIc(GrammarBase.component(node));
            case LH:
                return v * computeLh(GrammarBase.component(node));

            case REM:
                return v * computeEm(GrammarBase.component(node).getRootPane());
            case REX:
                return v * computeEx(GrammarBase.component(node).getRootPane());
            case RCAP:
                return v * computeCap(GrammarBase.component(node).getRootPane());
            case RCH:
                return v * computeCh(GrammarBase.component(node).getRootPane());
            case RIC:
                return v * computeIc(GrammarBase.component(node).getRootPane());
            case RLH:
                return v * computeLh(GrammarBase.component(node).getRootPane());

            case VW:
                return v * computeVw(GrammarBase.component(node));
            case VH:
                return v * computeVh(GrammarBase.component(node));
            case VMAX:
                return v * computeVmax(GrammarBase.component(node));
            case VMIN:
                return v * computeVmin(GrammarBase.component(node));
            case VB:
                return v * computeVb(GrammarBase.component(node));
            case VI:
                return v * computeVi(GrammarBase.component(node));

            default:
                throw new UnsupportedOperationException("Unsupported unit: " + unit);
        }
    }

    public static Number angle(String value) {
        return GrammarBase.parseSingle(ANGLE, "angle", value);
    }

    public static Number angle(Number number, CssUnitAngle unit) {
        double v = number.doubleValue();

        switch (unit) {
            case DEG:
                return number;
            case RAD:
                return 180.0 * v / Math.PI;
            case GRAD:
                return 0.9 * v;
            case TURN:
                return 360.0 * v;

            default:
                throw new UnsupportedOperationException("Unsupported unit: " + unit);
        }
    }

    private static double computeEm(JComponent component) {
        return 1.3333 * component.getFont().getSize();
    }

    private static double computeEx(JComponent component) {
        FontMetrics metrics = component.getFontMetrics(component.getFont());
        return metrics.getStringBounds("x", component.getGraphics()).getHeight();
    }

    private static double computeCap(JComponent component) {
        FontMetrics metrics = component.getFontMetrics(component.getFont());
        return metrics.getStringBounds("A", component.getGraphics()).getHeight();
    }

    private static double computeCh(JComponent component) {
        FontMetrics metrics = component.getFontMetrics(component.getFont());
        return metrics.getStringBounds("0", component.getGraphics()).getWidth();
    }

    private static double computeIc(JComponent component) {
        FontMetrics metrics = component.getFontMetrics(component.getFont());
        return metrics.getStringBounds("水", component.getGraphics()).getWidth();
    }

    private static double computeLh(JComponent component) {
        FontMetrics metrics = component.getFontMetrics(component.getFont());
        return metrics.getHeight();
    }

    private static double computeVh(JComponent component) {
        return component.getRootPane().getContentPane().getHeight() / 100.0;
    }

    private static double computeVw(JComponent component) {
        return component.getRootPane().getContentPane().getWidth() / 100.0;
    }

    private static double computeVmax(JComponent component) {
        Container pane = component.getRootPane().getContentPane();
        return Math.max(pane.getHeight(), pane.getWidth()) / 100.0;
    }

    private static double computeVmin(JComponent component) {
        Container pane = component.getRootPane().getContentPane();
        return Math.min(pane.getHeight(), pane.getWidth()) / 100.0;
    }

    private static double computeVb(JComponent component) {
        return ValueUtils.or(component.getParent(), component).getHeight() / 100.0;
    }

    private static double computeVi(JComponent component) {
        return ValueUtils.or(component.getParent(), component).getWidth() / 100.0;
    }

}
