package click.kamil.springstatemachineexporter.springbased;

import click.kamil.examples.statemachine.simple.OrderEvents;
import click.kamil.examples.statemachine.simple.OrderStates;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.StateMachineFactory;
import org.springframework.statemachine.state.PseudoStateKind;
import org.springframework.statemachine.state.State;
import org.springframework.statemachine.transition.Transition;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.File;
import java.io.PrintWriter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

public class SpringExporter {
}

@Component
@Slf4j
@RequiredArgsConstructor
class Runner implements ApplicationRunner {

    private final DotExporter dotExporter;
    private final PlantUmlExporter plantUmlExporter;
    private final ScxmlExporter scxmlExporter;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        dotExporter.export("stateMachine.dot");
        plantUmlExporter.export("stateMachine.uml.dot");
        scxmlExporter.export("stateMachine.scxml.xml");
    }
}

abstract class BaseExporter {

    protected final StateMachineFactory<OrderStates, OrderEvents> factory;

    protected BaseExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
        this.factory = factory;
    }

    abstract void export(String name) throws Exception;

    protected StateMachine<OrderStates, OrderEvents> createStateMachine() {
        return factory.getStateMachine("order");
    }
}

@Component
class DotExporter extends BaseExporter {

    public DotExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
        super(factory);
    }

    @Override
    public void export(String filename) throws Exception {
        try (PrintWriter writer = new PrintWriter(filename)) {
            StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
            writer.println("digraph stateMachine {");

            for (State<OrderStates, OrderEvents> state : machine.getStates()) {
                writer.printf("    %s;%n", state.getId());
            }

            for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
                if (transition.getSource() != null && transition.getTarget() != null) {
                    writer.printf("    %s -> %s [label=\"%s\"];%n",
                            transition.getSource().getId(),
                            transition.getTarget().getId(),
                            transition.getTrigger() != null ? transition.getTrigger().getEvent() : "");
                }
            }

            writer.println("}");
        }
    }
}

@Component
class PlantUmlExporter extends BaseExporter {

    public PlantUmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
        super(factory);
    }

    @Override
    public void export(String filename) throws Exception {
        try (PrintWriter writer = new PrintWriter(filename)) {
            StateMachine<OrderStates, OrderEvents> machine = createStateMachine();
            writer.println("@startuml");
            writer.println("[*] --> " + machine.getInitialState().getId());

            for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
                if (transition.getSource() != null && transition.getTarget() != null) {
                    String source = transition.getSource().getId().toString();
                    String target = transition.getTarget().getId().toString();
                    String label = "";

                    if (transition.getTrigger() != null && transition.getTrigger().getEvent() != null) {
                        label += transition.getTrigger().getEvent().toString();
                    }

                    if (transition.getGuard() != null) {
                        if (!label.isEmpty())
                            label += " ";
                        label += "[guard]";
                    }

                    writer.printf("%s --> %s%s%n", source, target, !label.isEmpty() ? " : " + label : "");
                }
            }

            for (State<OrderStates, OrderEvents> state : machine.getStates()) {
                if (state.getPseudoState() != null && state.getPseudoState().getKind() == PseudoStateKind.END) {
                    writer.printf("%s --> [*]%n", state.getId());
                }
            }

            writer.println("@enduml");
        }
    }
}

@Component
class ScxmlExporter extends BaseExporter {

    public ScxmlExporter(@Qualifier("simpleEnumStateMachineFactory") StateMachineFactory<OrderStates, OrderEvents> factory) {
        super(factory);
    }

    @Override
    public void export(String filename) throws Exception {
        StateMachine<OrderStates, OrderEvents> machine = createStateMachine();

        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        Document doc = dBuilder.newDocument();

        Element scxml = doc.createElement("scxml");
        scxml.setAttribute("version", "1.0");
        doc.appendChild(scxml);

        Element statesElement = doc.createElement("states");
        scxml.appendChild(statesElement);

        for (State<OrderStates, OrderEvents> state : machine.getStates()) {
            Element stateEl = doc.createElement("state");
            stateEl.setAttribute("id", state.getId().toString());
            statesElement.appendChild(stateEl);
        }

        Element transitionsElement = doc.createElement("transitions");
        scxml.appendChild(transitionsElement);

        for (Transition<OrderStates, OrderEvents> transition : machine.getTransitions()) {
            Element t = doc.createElement("transition");
            t.setAttribute("from", transition.getSource().getId().toString());
            t.setAttribute("to", transition.getTarget().getId().toString());
            t.setAttribute("event", String.valueOf(transition.getTrigger()));
            transitionsElement.appendChild(t);
        }

        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.transform(new DOMSource(doc), new StreamResult(new File(filename)));
    }
}
