import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.math.*;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.control.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.layout.*;
import javafx.scene.input.*;
import javafx.scene.*;
import javafx.util.*;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.geometry.*;
import javafx.collections.*;

public class Asn1Viewer extends Application {

  TextArea hexArea, valueArea;
  TreeTableView<Pair<Asn1Object, Integer>> treeTableView;
  GridPane gridPane;

  @Override
  public void start(Stage stage) {
    BorderPane layout = new BorderPane();
    stage.setScene(new Scene(layout, 800, 500));
    BorderPane cont = new BorderPane();
    hexArea = new TextArea();
    valueArea = new TextArea();
    valueArea.setEditable(false);
    valueArea.setWrapText(true);
    valueArea.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
      if (keyEvent.getCode() == KeyCode.TAB && !keyEvent.isControlDown()) {
        KeyEvent newEvent = new KeyEvent(keyEvent.getEventType(), keyEvent.getCharacter(),
          keyEvent.getText(), keyEvent.getCode(), keyEvent.isShiftDown(),
          !keyEvent.isControlDown(), keyEvent.isAltDown(), keyEvent.isMetaDown());
        Event.fireEvent(keyEvent.getTarget(), newEvent);
        keyEvent.consume();
      } 
    });
    treeTableView = createTreeTableView();
    gridPane = new GridPane();
    ScrollPane sp = new ScrollPane();
    sp.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
    sp.setContent(gridPane);
    sp.setFocusTraversable(true);
    SplitPane splitPane = new SplitPane(treeTableView, valueArea);
    SplitPane.setResizableWithParent(valueArea, false);
    splitPane.setOrientation(Orientation.VERTICAL);
    splitPane.setDividerPositions(1d);
    cont.setCenter(splitPane);
    cont.setRight(sp);
    layout.setCenter(cont);
    Button button = new Button("Load");
    button.setOnAction(event -> { load(stage); treeTableView.requestFocus(); });
    HBox hb = new HBox();
    hb.getChildren().addAll(button);
    layout.setBottom(hb);
    BorderPane.setMargin(hb, new Insets(2));
    setContent(createDemoAsn1Obj());
    stage.show();
  }

  void setContent(Asn1Object root) {
    TreeItem<Pair<Asn1Object, Integer>> tiRoot = createTreeItem(root);
    treeTableView.setRoot(tiRoot);
    treeTableView.getSelectionModel().selectFirst();
  }

  void load(Stage stage) {
    Stage newStage = new Stage();
    newStage.initOwner(stage);
    newStage.initModality(Modality.APPLICATION_MODAL);
    BorderPane layout = new BorderPane();
    newStage.setScene(new Scene(layout, 700, 300));
    newStage.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
      if (keyEvent.getCode() == KeyCode.ESCAPE) newStage.close();
    });
    hexArea.setFont(Font.font("Monospaced"));
    layout.setCenter(hexArea);
    Button button1 = new Button("File");
    button1.setOnAction((event) -> { fromFile(newStage); newStage.requestFocus(); hexArea.requestFocus(); });
    Button button2 = new Button("Clear");
    button2.setOnAction((event) -> { hexArea.clear(); hexArea.requestFocus(); });
    Button button3 = new Button("Paste");
    button3.setOnAction((event) -> { hexArea.paste(); hexArea.requestFocus(); });
    Button button4 = new Button("Ok");
    button4.setOnAction((event) -> { if (loadDump(newStage)) newStage.close(); hexArea.requestFocus(); });
    HBox hb = new HBox(4);
    hb.getChildren().addAll(button1, button2, button3, button4);
    layout.setBottom(hb);
    BorderPane.setMargin(hb, new Insets(2));
    newStage.show();
  }

  void fromFile(Stage stage) {
    FileChooser fileChooser = new FileChooser();
    File file = fileChooser.showOpenDialog(stage);
    if (file != null) {
      try (RandomAccessFile fl = new RandomAccessFile(file, "r")) {
        StringBuilder sb = new StringBuilder();
        boolean bin = false;
        int b = -1;
        while ((b = fl.read()) != -1) {
          if ((b < 32 || b > 127) && b != 9 && b != 10 && b != 13) { bin = true; break; }
          sb.append((char)b);
        }
        if (bin) {
          sb.setLength(0);
          fl.seek(0);
          for (int i = 0; (b = fl.read()) != -1; i++) {
            sb.append(String.format("%02X", b));
            sb.append(i % 16 == 15 ? "\n" : i % 16 == 7 ? "  " : " ");
          }
        }
        hexArea.setText(sb.toString());
      } catch (Exception e) { alertError(stage, e.toString()); }
    }
  }

  void alertError(Window window, String s) {
    Alert alert = new Alert(Alert.AlertType.ERROR, s, ButtonType.OK);
    alert.initOwner(window);
    alert.setHeaderText(null);
    alert.showAndWait();
  }

  boolean loadDump(Stage stage) {
    Exception e1 = null;
    try {
      byte[] source = {};
      String s = hexArea.getText().replaceAll("-----BEGIN [^-]+-----\\s+|-----END [^-]+-----|begin-base64[^\\n]+\\s+|====", "");
      if (hexArea.getText().length() == s.length()) {
        try {
          s = hexArea.getText().replaceAll("\\s", "");
          source = new byte[s.length() / 2];
          for (int i = 0; i < source.length; i++) {
            int index = i * 2;
            int v = Integer.parseInt(s.substring(index, index + 2), 16);
            source[i] = (byte)v;
          }
          try {
            Asn1Object o = new Asn1Object(source);
            setContent(o);
            return true;
          } catch (Exception e) { e1 = e; }
        } catch (Exception e) {}
      }
      source = Base64.getMimeDecoder().decode(s);
      Asn1Object o = new Asn1Object(source);
      setContent(o);
    } catch (Exception e) {
      alertError(stage, (e1 != null ? e1 : e).toString());
      return false;
    }
    return true;
  }

  TreeTableView<Pair<Asn1Object, Integer>> createTreeTableView() {
    TreeTableView<Pair<Asn1Object, Integer>> treeTableView = new TreeTableView<>();
    TreeTableColumn<Pair<Asn1Object, Integer>, Pair<Asn1Object, Integer>> column = new TreeTableColumn<>();
    column.setCellValueFactory(
      (TreeTableColumn.CellDataFeatures<Pair<Asn1Object, Integer>, Pair<Asn1Object, Integer>> cellData) -> 
      new ReadOnlyObjectWrapper<Pair<Asn1Object, Integer>>(cellData.getValue().getValue())
    );
    column.setCellFactory(c -> {
      TreeTableCell<Pair<Asn1Object, Integer>, Pair<Asn1Object, Integer>> cell = new TreeTableCell<Pair<Asn1Object, Integer>, Pair<Asn1Object, Integer>>() {
        @Override
        protected void updateItem(Pair<Asn1Object, Integer> item, boolean empty) {
          super.updateItem(item, empty);
          if (empty || item == null || item.getValue() == null || item.getKey() == null) {
            setGraphic(null);
          } else {
            Text text1 = new Text(item.getKey().toString() +" Offset: "+item.getValue().intValue());
            Text text2 = new Text("  " + item.getKey().valueAsString());
            text2.setFill(Color.BLUE);
            setGraphic(new Group(new TextFlow(text1, text2)));
          }
        }
      };
      return cell;
    });
    treeTableView.getColumns().add(column);
    treeTableView.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
    treeTableView.widthProperty().addListener((ov, t, t1) -> {
      Pane header = (Pane)treeTableView.lookup("TableHeaderRow");
      if (header != null && header.isVisible()) {
        header.setMaxHeight(0); header.setMinHeight(0); header.setPrefHeight(0);
        header.setVisible(false); header.setManaged(false);
      }
    });
    treeTableView.getFocusModel().focusedItemProperty().addListener((observable, oldValue, newValue) -> {
      if (gridPane == null || gridPane.getChildren().isEmpty()) return;
      if (oldValue != null) showTag(oldValue.getValue(), false);
      if (newValue != null) showTag(newValue.getValue(), true);
      if (newValue != null && newValue.getValue() != null && newValue.getValue().getKey() != null) {
        String s = newValue.getValue().getKey().valueAsString(Asn1Object.STVALUE);
        valueArea.setText(s);
      } else valueArea.clear();
    });
    return treeTableView;
  }

  void showTag(Pair<Asn1Object, Integer> value, boolean sel) {
    if (value != null && value.getValue() != null && value.getKey() != null) {
      Asn1Object o = value.getKey();
      int offset = value.getValue().intValue();
      Background bg1 = new Background(new BackgroundFill(Color.web("C0C0C0"), CornerRadii.EMPTY, Insets.EMPTY));
      Background bg2 = new Background(new BackgroundFill(Color.web("DCDCDC"), CornerRadii.EMPTY, Insets.EMPTY));
      ObservableList<Node> ol = gridPane.getChildren();
      for(int i = 0, hl = o.headLength(); i < hl + o.getLength() && offset + i < ol.size(); i++) {
        ((StackPane)ol.get(offset + i)).setBackground(sel ? i < hl ? bg1 : bg2 : Background.EMPTY);
      }
    }
  }

  Font font;
  int offset, n;
  TreeItem<Pair<Asn1Object, Integer>> createTreeItem(Asn1Object root) {
    if (root == null) return null;
    gridPane.getChildren().clear();
    font = Font.font("Monospaced");
    offset = n = 0;
    try { 
      return walkTree(root);
    } catch (Exception e) { return null; }
  }

  TreeItem<Pair<Asn1Object, Integer>> walkTree(Asn1Object o) throws IOException {
    Iterator<Asn1Object> iter = o.getSubElementsIterator();
    TreeItem<Pair<Asn1Object, Integer>> ti = new TreeItem<>();
    ti.setValue(new Pair<Asn1Object, Integer>(o, Integer.valueOf(offset)));
    offset += o.effHeadLength();
    ti.setExpanded(true);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    o.writeObj(out);
    byte[] bytes = out.toByteArray();
    for (int hl = o.headLength(), i = 0; i < bytes.length; i++, n++) {
      String s = String.format("%02X", bytes[i]);
      Text text = new Text(s + (n % 16 == 15 ? "" : n % 16 == 7 ? "  " : " "));
      text.setFont(font);
      text.setFill(i < hl ? Color.BLACK : Color.BLUE);
      gridPane.add(new StackPane(text), n % 16, n / 16);
      text.setOnMousePressed(event -> { goToItem(ti); event.consume(); });
    }
    while (iter.hasNext()) ti.getChildren().add(walkTree(iter.next()));
    return ti;
  }

  void goToItem(TreeItem<Pair<Asn1Object, Integer>> ti) {
    if (ti != null) {
      treeTableView.getSelectionModel().select(ti);
      treeTableView.requestFocus();
    }
  }

  Asn1Object createDemoAsn1Obj() {
    Asn1Object o1 =
      new Asn1Object(Asn1Object.CONSTRUCTED, Asn1Object.SEQUENCE).addAll(
        new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.BOOLEAN, Boolean.valueOf(true)),
        new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.UTC_TIME, new Date())
      );
    Asn1Object o2 =
      new Asn1Object(Asn1Object.CONSTRUCTED, Asn1Object.SEQUENCE).addAll(
        new Asn1Object(Asn1Object.CONSTRUCTED, Asn1Object.SET).addAll(
          new Asn1Object(Asn1Object.CONSTRUCTED, Asn1Object.SEQUENCE).addAll(
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.OBJECT_IDENTIFIER, "2.12345.12345.12345"),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.UTF8_STRING, "qwertyu"),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.OCTET_STRING, "octet".getBytes()),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.BIT_STRING, new Object[] { Integer.valueOf(4), "bit".getBytes() }),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.OCTET_STRING, o1.getBytes()),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.BIT_STRING, new Object[] { Integer.valueOf(0), o1.getBytes() })
          ),
          new Asn1Object(Asn1Object.CONSTRUCTED, Asn1Object.SEQUENCE).setIndefinite(true).addAll(
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.INTEGER, new BigInteger("12345678901234567890")),
            new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.INTEGER, Integer.valueOf(-1234567890)),
            new Asn1Object(Asn1Object.EOC, Asn1Object.EOC)
          ),
          new Asn1Object(Asn1Object.CONTEXT, 0xABCD, "abcd".getBytes())
        ),
        new Asn1Object(Asn1Object.UNIVERSAL, Asn1Object.NULL)
      );
      //try (OutputStream os = Base64.getMimeEncoder().wrap(new FileOutputStream("demo.base64"))) {
      //  o2.write(os);
      //} catch (Exception ex) { }
    return o2;
  }

} // End of class Asn1Viewer