package net.ranides.assira.ui.parsers;

import lombok.experimental.UtilityClass;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.grammar.G4Selector;
import net.ranides.assira.grammar.G4SelectorBuilder;
import net.ranides.assira.ui.components.CustomBevelBorder;
import net.ranides.assira.ui.model.CssBorderSize;
import net.ranides.assira.ui.model.CssBorderStyle;
import net.ranides.grammars.generated.UiGrammarParser;

import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.border.SoftBevelBorder;
import java.awt.*;
import java.util.List;
import java.util.function.Supplier;

import static net.ranides.assira.ui.parsers.ParseCssColor.COLOR;

@UtilityClass
public class ParseCssBorder {

    public static final G4Selector<CssBorderStyle> BORDER_STYLE = new G4SelectorBuilder<CssBorderStyle>()
        .withName("CSS:border-style")
        .withParser(GrammarBase.PARSER)
        .valueForToken(UiGrammarParser.CSS2_none, CssBorderStyle.NONE)
        .valueForToken(UiGrammarParser.CSS2_BORDER_solid, CssBorderStyle.SOLID)
        .valueForToken(UiGrammarParser.CSS2_BORDER_dotted, CssBorderStyle.DOTTED)
        .valueForToken(UiGrammarParser.CSS2_BORDER_dashed, CssBorderStyle.DASHED)
        .valueForToken(UiGrammarParser.CSS2_BORDER_double, CssBorderStyle.DOUBLE)
        .valueForToken(UiGrammarParser.CSS2_BORDER_groove, CssBorderStyle.GROOVE)
        .valueForToken(UiGrammarParser.CSS2_BORDER_ridge, CssBorderStyle.RIDGE)
        .valueForToken(UiGrammarParser.CSS2_BORDER_inset, CssBorderStyle.INSET)
        .valueForToken(UiGrammarParser.CSS2_BORDER_outset, CssBorderStyle.OUTSET)
        .valueForToken(UiGrammarParser.SWING_BORDER_etched, CssBorderStyle.SWING_ETCHED)
        .valueForToken(UiGrammarParser.SWING_BORDER_bevel, CssBorderStyle.SWING_BEVEL)
        .valueForToken(UiGrammarParser.SWING_BORDER_bevel_soft, CssBorderStyle.SWING_BEVEL_SOFT)
        .prepare();

    public static final G4Selector<Integer> SIZE_COMPONENT = new G4SelectorBuilder<Integer>()
        .withName("CSS:border-size")
        .withParser(GrammarBase.PARSER)
        .inherit(ParseCssUnit.LENGTH)
        .valueForToken(UiGrammarParser.CSS2_BORDER_thin, 1)
        .valueForToken(UiGrammarParser.CSS2_BORDER_medium, 2)
        .valueForToken(UiGrammarParser.CSS2_BORDER_thick, 3)
        .prepare();


    public static final G4Selector<CssBorderSize> BORDER_SIZE = new G4SelectorBuilder<CssBorderSize>()
        .withName("CSS:border-size")
        .withParser(GrammarBase.PARSER)
        .mapRule("#borderSize1", node -> {
            int s = SIZE_COMPONENT.single(node);
            return CssBorderSize.of(s);
        })
        .mapRule("#borderSize2", node -> {
            java.util.List<Integer> s = SIZE_COMPONENT.list(node);
            return CssBorderSize.of(s.get(0), s.get(1));
        })
        .mapRule("#borderSize4", node -> {
            List<Integer> s = SIZE_COMPONENT.list(node);
            return CssBorderSize.of(s.get(0), s.get(1), s.get(2), s.get(3));
        })

        .prepare();

    public static final G4Selector<Boolean> SWING_RAISED = new G4SelectorBuilder<Boolean>()
        .withName("ParseCssBorder:SWING_RAISED")
        .withParser(GrammarBase.PARSER)
        .valueForToken(UiGrammarParser.SWING_BORDER_raised, true)
        .valueForToken(UiGrammarParser.SWING_BORDER_lowered, false)
        .prepare();

    public static final G4Selector<Border> BORDER = new G4SelectorBuilder<Border>()
        .withName("CSS:border")
        .withParser(GrammarBase.PARSER)
        .mapRule("borderCSS", node -> {
            Ref<CssBorderStyle> style = node.children("borderCSS_Style").map(BORDER_STYLE::single).single();
            Ref<Color> color = node.children("color").map(COLOR::single).single();
            Ref<CssBorderSize> size = node.children("borderSize").map(BORDER_SIZE::single).single();

            switch (style.orElse(CssBorderStyle.SOLID)) {
                case SOLID:
                    return createSolid(color, size, () -> GrammarBase.component(node));
                case OUTSET:
                    return createBevel(color, size, CustomBevelBorder.RAISED);
                case INSET:
                    return createBevel(color, size, CustomBevelBorder.LOWERED);
                case DASHED:
                    return createDashed(color, size, () -> GrammarBase.component(node));
                case DOTTED:
                    return createDotted(color, size, () -> GrammarBase.component(node));
                case DOUBLE:
                    return createDouble(color, size, () -> GrammarBase.component(node));
                case GROOVE:
                    return createGroove(color, size);
                case RIDGE:
                    return createRidge(color, size);
                case NONE:
                    return createNone(size);
                default:
                    throw new UnsupportedOperationException("Unsupported border style: " + style);
            }


        })
        .mapRule("borderSwingSimple", node -> {
            CssBorderStyle style = BORDER_STYLE.first(node.child("borderSwingSimple_Style"));
            List<Color> colors = node.children("color").map(v -> COLOR.single(v)).list();
            boolean raised = SWING_RAISED.single(node.child("borderSwingSimple_Type"));

            switch (style) {
                case SWING_ETCHED:
                    return createSwingEtched(colors, raised);
                case SWING_BEVEL:
                    return createSwingBevel(colors, raised);
                case SWING_BEVEL_SOFT:
                    return createSwingBevelSoft(colors, raised);
                default:
                    throw new UnsupportedOperationException("Unsupported border style: " + style);
            }
        })
        .prepare();

    private static Border createSolid(Ref<Color> color, Ref<CssBorderSize> size, Supplier<JComponent> component) {
        Color icolor = color.orElseGet(() -> component.get().getForeground());
        CssBorderSize isize = size.orElse(CssBorderSize.SINGLE);
        if (isize.isSimple()) {
            return BorderFactory.createLineBorder(icolor, isize.getSimpleSize());
        } else {
            return BorderFactory.createMatteBorder(isize.getTop(), isize.getLeft(), isize.getBottom(), isize.getRight(), icolor);
        }
    }

    private static Border createBevel(Ref<Color> color, Ref<CssBorderSize> size, int type) {
        if(size.isAbsent() && color.isAbsent()) {
            return BorderFactory.createBevelBorder(type);
        }
        if(color.isAbsent()) {
            return new CustomBevelBorder(type, size.map(CssBorderSize::getSimpleSize).orElse(1), null, null);
        } else {
            return new CustomBevelBorder(type, size.map(CssBorderSize::getSimpleSize).orElse(1), color.get().brighter(), color.get().darker());
        }
    }

    private static Border createDashed(Ref<Color> color, Ref<CssBorderSize> size, Supplier<JComponent> component) {
        Color icolor = color.orElseGet(() -> component.get().getForeground());
        int isize = size.orElse(CssBorderSize.SINGLE).getSimpleSize();
        return BorderFactory.createDashedBorder(icolor, isize, 3.0f, 3.0f, false);
    }

    private static Border createDotted(Ref<Color> color, Ref<CssBorderSize> size, Supplier<JComponent> component) {
        Color icolor = color.orElseGet(() -> component.get().getForeground());
        int isize = size.orElse(CssBorderSize.SINGLE).getSimpleSize();
        return BorderFactory.createDashedBorder(icolor, isize, 1.0f, 1.0f, true);
    }

    private static CompoundBorder createDouble(Ref<Color> color, Ref<CssBorderSize> size, Supplier<JComponent> component) {
        Color icolor = color.orElseGet(() -> component.get().getForeground());
        int isize = size.orElse(CssBorderSize.SINGLE).getSimpleSize();

        return BorderFactory.createCompoundBorder(
            BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(icolor, isize),
                BorderFactory.createEmptyBorder(isize, isize, isize, isize)
            ),
            BorderFactory.createLineBorder(icolor, isize)
        );
    }

    private static CompoundBorder createGroove(Ref<Color> color, Ref<CssBorderSize> size) {
        return BorderFactory.createCompoundBorder(createBevel(color, size, CustomBevelBorder.LOWERED),
            createBevel(color, size, CustomBevelBorder.RAISED)
        );
    }

    private static CompoundBorder createRidge(Ref<Color> color, Ref<CssBorderSize> size) {
        return BorderFactory.createCompoundBorder(createBevel(color, size, CustomBevelBorder.RAISED),
            createBevel(color, size, CustomBevelBorder.LOWERED)
        );
    }

    private static Border createNone(Ref<CssBorderSize> size) {
        int isize = size.orElse(CssBorderSize.SINGLE).getSimpleSize();
        return BorderFactory.createEmptyBorder(isize, isize, isize, isize);
    }

    private static Border createSwingEtched(List<Color> colors, boolean raised) {
        int etchedType = raised ? EtchedBorder.RAISED : EtchedBorder.LOWERED;
        if (colors.size() == 2) {
            return BorderFactory.createEtchedBorder(etchedType, colors.get(0), colors.get(1));
        }
        else if (colors.size() == 1) {
            return BorderFactory.createEtchedBorder(etchedType, colors.get(0).brighter(), colors.get(0).darker());
        }
        else if (colors.size() == 0) {
            return BorderFactory.createEtchedBorder(etchedType);
        }
        else {
            // shouldn't happen: grammar will check format
            throw new UnsupportedOperationException("Unsupported border syntax");
        }
    }

    private static Border createSwingBevel(List<Color> colors, boolean raised) {
        int bevelType = raised ? BevelBorder.RAISED : BevelBorder.LOWERED;
        if (colors.size() == 2) {
            return BorderFactory.createBevelBorder(bevelType, colors.get(0), colors.get(1));
        }
        else if (colors.size() == 1) {
            return BorderFactory.createBevelBorder(bevelType, colors.get(0).brighter(), colors.get(0).darker());
        }
        else if (colors.size() == 0) {
            return BorderFactory.createBevelBorder(bevelType);
        }
        else {
            // shouldn't happen: grammar will check format
            throw new UnsupportedOperationException("Unsupported border syntax");
        }
    }

    private static Border createSwingBevelSoft(List<Color> colors, boolean raised) {
        int softBevelType = raised ? SoftBevelBorder.RAISED : SoftBevelBorder.LOWERED;
        if (colors.size() == 2) {
            return BorderFactory.createSoftBevelBorder(softBevelType, colors.get(0), colors.get(1));
        }
        else if (colors.size() == 1) {
            return BorderFactory.createSoftBevelBorder(softBevelType, colors.get(0).brighter(), colors.get(0).darker());
        }
        else if (colors.size() == 0) {
            return BorderFactory.createSoftBevelBorder(softBevelType);
        }
        else {
            // shouldn't happen: grammar will check format
            throw new UnsupportedOperationException("Unsupported border syntax");
        }
    }

    public static Border border(String value) {
        return GrammarBase.parseSingle(BORDER, "root_border", value);
    }

    public static Border border(JComponent component, String value) {
        return GrammarBase.parseSingle(BORDER, "root_border", component, value);
    }
}
