import java.util.*;
import java.io.*;
import java.text.*;
import java.math.BigDecimal;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.*;
import javafx.scene.layout.*;
import javafx.collections.*;
import javafx.beans.property.*;
import javafx.geometry.*;
import javafx.scene.input.*;

public class DbfEdit extends Application {

  Stage stage;
  Label label;
  SmDBF dbf;
  String dStFormat = "yyyy/MM/dd";
  String tStFormat = "yyyy/MM/dd-HH:mm:ss";

  @Override
  public void start(Stage stage) {
    this.stage = stage;
    BorderPane layout = new BorderPane();
    stage.setScene(new Scene(layout, 700, 400));
    TableView<IntVal> tableView = new TableView<>();
    label = new Label();
    File file;
    List<String> p = getParameters().getRaw();
    if (!p.isEmpty()) {
      file = new File(p.get(0));
      if (!file.isFile()) throw new RuntimeException(file.getName() + " file not found");
    } else file = new File("demo.dbf");
    try {
      if (file.exists())
        dbf = new SmDBF(file);
      else {
        dbf = new SmDBF(file, new Object[][] {
          {"FIELD1_C", 'C', 20, 0},
          {"FIELD2_L", 'L',  1, 0},
          {"FIELD3_D", 'D',  8, 0},
          {"FIELD4_T", 'T', 14, 0},
          {"FIELD5_N", 'N', 15, 0},
          {"FIELD6_F", 'F',  7, 2}
        });
        //dbf.setCharset("cp437");
        for (int i = 0; i < 20; i++) {
          dbf.append();
          dbf.set(1, "Record " + i);
          dbf.set(2, true);
          dbf.set(3, new Date());
          dbf.set(4, new Date());
          dbf.set(5, new BigDecimal(i));
          dbf.set(6, new BigDecimal(i));
        }
      }
    } catch (Exception e) { throw new RuntimeException(e); }
    try {
      tableView.setItems(new VirtObservableList());
      for (int i = 1; i <= dbf.getFieldCount(); i++) {
        final int f = i;
        TableColumn<IntVal, IntVal> column = new TableColumn<>(dbf.getFieldName(f));
        column.setCellValueFactory(cd -> new ReadOnlyObjectWrapper<IntVal>(cd.getValue()));
        column.setCellFactory(c -> {
          TableCell<IntVal, IntVal> cell = new TableCell<IntVal, IntVal>() {

            @Override
            protected void updateItem(IntVal item, boolean empty) {
              super.updateItem(item, empty);
              if (item == null || empty) {
                setText(null);
                setStyle("");
              } else {
                String s;
                Object obj = null;
                dbf.goToRec(item.get() + 1);
                char type = dbf.getFieldType(f);
                if (type == 'M') s = SmDBF.typeToString(type);
                else {
                  try {
                    obj = dbf.get(f);
                    if (obj == null) {
                      s = "null";
                    } else if (obj instanceof Boolean) {
                      s = (Boolean)obj ? "T" : "F";
                    } else if (obj instanceof Date) {
                      s = new SimpleDateFormat(type == 'D' ? dStFormat : tStFormat).format((Date)obj);
                    } else if (obj instanceof String) {
                      s = SmDBF.rtrim((String)obj);
                    } else s = obj.toString();
                  } catch (Exception e) { s = "error"; }
                }
                if (type == 'N' || type == 'F') setAlignment(Pos.CENTER_RIGHT);
                try {
                  String style = dbf.isDeleted() ? "-fx-accent" : "-fx-text-base-color";
                  setStyle("-fx-text-fill: " + style);
                } catch (Exception e) { }
                setText(s);
              }
            }
          };
          return cell;
        });
        if (dbf.getFieldLength(f) > 30) column.setPrefWidth(200);
        tableView.getColumns().add(column);
      }
    } catch (Exception e) { e.printStackTrace(); }

    tableView.setOnKeyPressed(ke -> {
      try {
        VirtObservableList items = (VirtObservableList)tableView.getItems();
        if (ke.getCode() == KeyCode.INSERT) {
          Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Append record?");
          alert.initOwner(stage);
          alert.setHeaderText(null);
          Optional<ButtonType> result = alert.showAndWait();
          if (result.get() == ButtonType.OK){
            dbf.append();
            items.insert(items.size());
            tableView.getSelectionModel().selectLast();
            tableView.scrollTo(tableView.getSelectionModel().getSelectedIndices().get(0));
          }
        } else if (ke.getCode() == KeyCode.DELETE) {
          IntVal item = tableView.getSelectionModel().getSelectedItems().get(0);
          if (item != null) {
            dbf.goToRec(item.get() + 1);
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION, (dbf.isDeleted() ? "Recall" : "Delete") + " record?");
            alert.initOwner(stage);
            alert.setHeaderText(null);
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK){
              dbf.setDeleted(!dbf.isDeleted());
              @SuppressWarnings("unchecked") TablePosition<IntVal,?> oldPos = tableView.getSelectionModel().getSelectedCells().get(0);
              items.update(item.get());
              tableView.getSelectionModel().select(oldPos.getRow(), oldPos.getTableColumn());
            }
          }
        } else if (ke.getCode() == KeyCode.ENTER) {
          IntVal item = tableView.getSelectionModel().getSelectedItems().get(0);
          if (item != null) {
            dbf.goToRec(item.get() + 1);
            int c = tableView.getSelectionModel().getSelectedCells().get(0).getColumn() + 1;
            char t = dbf.getFieldType(c);
            if (t != 'M') {
              Object obj = dbf.get(c);
              String s = "", l = "";
              int len;
              if (t == 'C') {
                s = SmDBF.rtrim(obj.toString());
                len = dbf.getFieldLength(c);
                l = "Length: " + len;
              } else if (t == 'L') {
                s = obj == null ? "F" : (Boolean)obj ? "T" : "F";
                len = 1;
                l = "Value: T or F";
              } else if (t == 'D' || t == 'T') {
                String stFormat = t == 'D' ? dStFormat : tStFormat;
                s = new SimpleDateFormat(stFormat).format(obj == null ? new Date() : (Date)obj);
                len = stFormat.length();
                l = "Format: " + stFormat.toUpperCase();
              } else if (t == 'N' || t == 'F') {
                s = obj == null ? "0" : obj.toString();
                len = dbf.getFieldLength(c);
                l = "Length: " + len + (dbf.getFieldLengthDecimal(c) > 0 ? "." + dbf.getFieldLengthDecimal(c) : "");
              } else len = 0;
              TextInputDialog dialog = new TextInputDialog(s);
              dialog.initOwner(stage);
              dialog.setHeaderText(null);
              dialog.setContentText(dbf.getFieldName(c));
              dialog.getDialogPane().setExpandableContent(new Label("Type: " + SmDBF.typeToString(t) + ", " + l));
              dialog.getDialogPane().setExpanded(true);
              TextField tf = dialog.getEditor();
              tf.textProperty().addListener((observable, oldValue, newValue) -> {
                if (tf.getText().length() > len) tf.setText(tf.getText().substring(0, len));
              });
              //if (t == 'N' || t == 'F') tf.setAlignment(Pos.CENTER_RIGHT);
              Optional<String> result = dialog.showAndWait();
              if (result.isPresent()) {
                obj = null;
                try {
                  if (t == 'C') {
                    obj = result.get();
                  } else if (t == 'L') {
                    if (result.get().equalsIgnoreCase("T")) obj = Boolean.valueOf(true);
                    else if (result.get().equalsIgnoreCase("F")) obj = Boolean.valueOf(false);
                  } else if (t == 'D' || t == 'T') {
                    obj = new SimpleDateFormat(t == 'D' ? dStFormat : tStFormat).parse(result.get());
                  } else if (t == 'N' || t == 'F') {
                    obj = new BigDecimal(result.get());
                  }
                } catch (Exception e) { }
                try { dbf.set(c, obj); } catch (Exception e) {
                  Alert alert = new Alert(Alert.AlertType.ERROR, e.toString(), ButtonType.OK);
                  alert.initOwner(stage);
                  alert.setHeaderText(null);
                  alert.showAndWait();
                }
                @SuppressWarnings("unchecked") TablePosition<IntVal,?> oldPos = tableView.getSelectionModel().getSelectedCells().get(0);
                items.update(item.get());
                tableView.getSelectionModel().select(oldPos.getRow(), oldPos.getTableColumn());
              }
            }
          }
        }
      } catch (Exception e) { }
    });
    tableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
    tableView.getSelectionModel().setCellSelectionEnabled(true);
    tableView.sortPolicyProperty().set(t -> false);

    @SuppressWarnings("rawtypes") ObservableList<TablePosition> selectedCells = tableView.getSelectionModel().getSelectedCells();
    selectedCells.addListener((@SuppressWarnings("rawtypes") ListChangeListener.Change<? extends TablePosition> change) -> {
      if (selectedCells.size() > 0) {
        TablePosition<?,?> pos = (TablePosition<?,?>)selectedCells.get(0);
        label.setText(String.format("Field: %5d / %d   Record: %7d / %d", pos.getColumn() + 1, dbf.getFieldCount(), pos.getRow() + 1, dbf.getRecCount()));
      } else {
        label.setText("");
      }
    });
    tableView.getSelectionModel().select(0, tableView.getColumns().get(0));
    layout.setCenter(tableView);
    layout.setBottom(label);

    stage.setOnCloseRequest(event -> { try { dbf.close(); } catch (Exception e) { } });

    stage.show();
  }

  public class IntVal {
    private int value;
    public IntVal(int value) { this.value = value; }
    public void set(int value) { this.value = value; }
    public int get() { return value; }
  }  // End of class IntVal

  public class VirtObservableList extends ObservableListBase<IntVal> {
    IntVal value = new IntVal(-1);

    @Override
    public int size() {
      return dbf.getRecCount();
    }

    @Override
    public IntVal get(int index) {
      if (index < 0 || index >= size()) throw new IndexOutOfBoundsException();
      if (index != value.get()) {
        value = new IntVal(index);
      }
      return value;
    }

    void update(int index) {
      beginChange();
      nextUpdate(index);
      endChange();
      value.set(-1);
    }

    void insert(int index) {
      beginChange();
      nextAdd(index, index + 1);
      endChange();
      value.set(-1);
    }

  } // End of class VirtObservableList

} // End of class DbfEdit